├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── amplify.yml ├── assets ├── agentic-assistant-workshop-architecture-lab-02.png ├── agentic-assistant-workshop-architecture-lab-03.png ├── agentic-assistant-workshop-architecture-lab-04.png ├── agentic-assistant-workshop-architecture-lab-05.png ├── agentic-assistant-workshop-architecture.png ├── assistant-ui-demo.png └── trigger-a-build-of-amplify-app.png ├── code_editor ├── .gitignore ├── README.md ├── bin │ └── setup-sagemaker-domain.ts ├── cdk.context.json ├── cdk.json ├── jest.config.js ├── lib │ └── setup-sagemaker-domain-stack.ts ├── package-lock.json ├── package.json ├── scripts │ ├── install-docker-lcc-script.sh │ └── install-lcc-script.sh ├── test │ └── setup-sagemaker-domain.test.ts └── tsconfig.json ├── data_pipelines ├── 01-validate-sagemaker-jobs-connection-to-postgreSQL.ipynb ├── 02-download-raw-pdf-documents.ipynb ├── 03-document-extraction.ipynb ├── 04-create-and-load-embeddings-into-aurora-postgreSQL.ipynb ├── 05-load-sql-tables-into-aurora-postgreSQL.ipynb ├── 06-sagemaker-pipeline-for-documents-processing.ipynb ├── 99-populate-rag-db.ipynb ├── README.md ├── data │ ├── documents_processed.json │ └── extracted_entities.csv ├── docker │ ├── Dockerfile │ └── requirements.txt ├── scripts │ ├── check_connection_to_aurora_postgres.py │ ├── load_sql_tables.py │ ├── prepare_and_load_data_in_aurora.py │ ├── prepare_and_load_embeddings.py │ ├── prepare_documents.py │ └── requirements.txt └── utils │ ├── __init__.py │ └── helpers.py ├── frontend ├── .gitignore ├── README.md ├── bin │ └── amplify-chatui.ts ├── cdk.json ├── chat-app │ ├── .env │ ├── .eslintrc.json │ ├── .gitignore │ ├── README.md │ ├── amplify.yml │ ├── app │ │ ├── aws-exports.tsx │ │ ├── components │ │ │ ├── ChatApp.tsx │ │ │ ├── ChatMessage.tsx │ │ │ ├── ClearButton.tsx │ │ │ ├── DebugToggleSwitch.tsx │ │ │ └── SelectMode.tsx │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.tsx │ │ └── page.tsx │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.js │ ├── tailwind.config.ts │ └── tsconfig.json ├── jest.config.js ├── lib │ └── amplify-chatui-stack.ts ├── package-lock.json ├── package.json ├── test │ └── amplify-chatui.test.ts └── tsconfig.json ├── lab_01_llm_apps_development_tools ├── README.md ├── lab1.1_cloudformation_s3.yaml └── lab1.2_s3_cdk │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── bin │ └── s3_cdk.ts │ ├── cdk.json │ ├── jest.config.js │ ├── lib │ └── s3_cdk-stack.ts │ ├── package-lock.json │ ├── package.json │ ├── test │ └── s3_cdk.test.ts │ └── tsconfig.json ├── lab_02_serverless_llm_assistant_setup ├── README.md └── invoke_assistant_lambda.py ├── lab_03_agentic_llm_assistant └── README.md ├── lab_04_agent_with_RAG └── README.md ├── lab_05_agent_with_analytics └── README.md └── serverless_llm_assistant ├── .gitignore ├── .npmignore ├── README.md ├── bin └── serverless_llm_assistant.ts ├── cdk.json ├── jest.config.js ├── lib ├── assistant-api-gateway.ts ├── assistant-authorizer.ts ├── assistant-sagemaker-iam-policy.ts ├── assistant-sagemaker-postgres-acess.ts ├── assistant-sagemaker-processor.ts ├── assistant-vpc.ts ├── lambda-functions │ └── agent-executor-lambda-container │ │ ├── Dockerfile │ │ ├── agent-executor-lambda │ │ ├── assistant │ │ │ ├── __init__.py │ │ │ ├── calculator.py │ │ │ ├── config.py │ │ │ ├── prompts.py │ │ │ ├── sql_chain.py │ │ │ ├── sqlqa.py │ │ │ ├── tools.py │ │ │ └── utils.py │ │ └── handler.py │ │ └── requirements.txt └── serverless_llm_assistant-stack.ts ├── package-lock.json ├── package.json ├── test └── serverless_llm_assistant.test.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/build-an-agentic-llm-assistant/1e4b2bea103b0af9b4f9aeffa620231dc1867002/.gitignore -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Build an Agentic LLM assistant on AWS 2 | 3 | This hands-on workshop, aimed at developers and solution builders, trains you on how to build a real-life serverless LLM application using foundation models (FMs) through Amazon Bedrock and advanced design patterns such as: Reason and Act (ReAct) Agent, text-to-SQL, and Retrieval Augemented Generation (RAG). 4 | It complements the [Amazon Bedrock Workshop](https://github.com/aws-samples/amazon-bedrock-workshop) by helping you transition from practicing standalone design patterns in notebooks to building an end-to-end llm serverless application. 5 | 6 | Within the labs of this workshop, you'll explore some of the most common and advanced LLM applications design patterns used by customers to improve business operations with Generative AI. 7 | Namely, these labs together help you build step by step a complex Agentic LLM assistant capable of answering retrieval and analytical questions on your internal knowledge bases. 8 | 9 | * Lab 1: Explore IaC with AWS CDK to streamline building LLM applications on AWS 10 | * Lab 2: Build a basic serverless LLM assistant with AWS Lambda and Amazon Bedrock 11 | * Lab 3: Refactor the LLM assistant in AWS Lambda into a custom LLM agent with basic tools 12 | * Lab 4: Extend the LLM agent with semantic retrieval from internal knowledge bases 13 | * Lab 5: Extend the LLM agent with the ability to query a SQL database 14 | 15 | Throughout these labs, you will be using and extending the CDK stack of the **Serverless LLM Assistant** available under the folder `serverless_llm_assistant`. 16 | 17 | ## Prerequisites 18 | 19 | 1. [Create an AWS Cloud9 environment](https://docs.aws.amazon.com/cloud9/latest/user-guide/tutorial-create-environment.html) to use as an IDE. 20 | 2. Configure [model access](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html) on Amazon Bedrock console, namely to access Amazon Titan and Anthropic Claude models on `us-west-2 (Oregon)`. 21 | 3. Setup an Amazon SageMaker Studio environment, using the [Quick setup for single users](https://docs.aws.amazon.com/sagemaker/latest/dg/onboard-quick-start.html#onboard-quick-start-instructions), to run the data-pipelines notebooks. 22 | 23 | Once ready, clone this repository into the new Cloud9 environment and follow lab instructions. 24 | 25 | ## Architecture 26 | 27 | The following diagram illustrates the target architecture of this workshop: 28 | 29 | ![Agentic Assistant workshop Architecture](/assets/agentic-assistant-workshop-architecture.png) 30 | 31 | ## Next step 32 | 33 | You can build on the knowledge acquired in this workshop by solving a more complex problem that requires studying the limitation of the popular design patterns used in llm application development and desiging a solution to overcome these limitations. 34 | For this, we propose that you read through the blog post [Boosting RAG-based intelligent document assistants using entity extraction, SQL querying, and agents with Amazon Bedrock 35 | ](https://aws.amazon.com/blogs/machine-learning/boosting-rag-based-intelligent-document-assistants-using-entity-extraction-sql-querying-and-agents-with-amazon-bedrock/) and explore its associated GitHub repository [aws-agentic-document-assistant](https://github.com/aws-samples/aws-agentic-document-assistant/). 36 | 37 | ## Security 38 | 39 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 40 | 41 | ## License 42 | 43 | This library is licensed under the MIT-0 License. See the LICENSE file. 44 | -------------------------------------------------------------------------------- /amplify.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | applications: 3 | - appRoot: frontend/chat-app 4 | frontend: 5 | phases: 6 | preBuild: 7 | commands: 8 | - ls -alh 9 | - node -v 10 | - npm -v 11 | - npm ci 12 | build: 13 | commands: 14 | - env | grep -e NEXT_PUBLIC_ >> .env.production 15 | - npm run build 16 | - ls -alh .next 17 | - ls -alh . 18 | artifacts: 19 | baseDirectory: .next 20 | files: 21 | - "**/*" 22 | cache: 23 | paths: 24 | - node_modules/**/* 25 | - .next/cache/**/* 26 | backend: 27 | phases: 28 | build: 29 | commands: 30 | - amplifyPush --simple 31 | -------------------------------------------------------------------------------- /assets/agentic-assistant-workshop-architecture-lab-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/build-an-agentic-llm-assistant/1e4b2bea103b0af9b4f9aeffa620231dc1867002/assets/agentic-assistant-workshop-architecture-lab-02.png -------------------------------------------------------------------------------- /assets/agentic-assistant-workshop-architecture-lab-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/build-an-agentic-llm-assistant/1e4b2bea103b0af9b4f9aeffa620231dc1867002/assets/agentic-assistant-workshop-architecture-lab-03.png -------------------------------------------------------------------------------- /assets/agentic-assistant-workshop-architecture-lab-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/build-an-agentic-llm-assistant/1e4b2bea103b0af9b4f9aeffa620231dc1867002/assets/agentic-assistant-workshop-architecture-lab-04.png -------------------------------------------------------------------------------- /assets/agentic-assistant-workshop-architecture-lab-05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/build-an-agentic-llm-assistant/1e4b2bea103b0af9b4f9aeffa620231dc1867002/assets/agentic-assistant-workshop-architecture-lab-05.png -------------------------------------------------------------------------------- /assets/agentic-assistant-workshop-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/build-an-agentic-llm-assistant/1e4b2bea103b0af9b4f9aeffa620231dc1867002/assets/agentic-assistant-workshop-architecture.png -------------------------------------------------------------------------------- /assets/assistant-ui-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/build-an-agentic-llm-assistant/1e4b2bea103b0af9b4f9aeffa620231dc1867002/assets/assistant-ui-demo.png -------------------------------------------------------------------------------- /assets/trigger-a-build-of-amplify-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/build-an-agentic-llm-assistant/1e4b2bea103b0af9b4f9aeffa620231dc1867002/assets/trigger-a-build-of-amplify-app.png -------------------------------------------------------------------------------- /code_editor/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | -------------------------------------------------------------------------------- /code_editor/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your CDK TypeScript project 2 | 3 | This is a blank project for CDK development with TypeScript. 4 | 5 | The `cdk.json` file tells the CDK Toolkit how to execute your app. 6 | 7 | ## Useful commands 8 | 9 | * `npm run build` compile typescript to js 10 | * `npm run watch` watch for changes and compile 11 | * `npm run test` perform the jest unit tests 12 | * `npx cdk deploy` deploy this stack to your default AWS account/region 13 | * `npx cdk diff` compare deployed stack with current state 14 | * `npx cdk synth` emits the synthesized CloudFormation template 15 | -------------------------------------------------------------------------------- /code_editor/bin/setup-sagemaker-domain.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from 'aws-cdk-lib'; 4 | import { SagemakerDomainWithCodeEditorStack } from '../lib/setup-sagemaker-domain-stack'; 5 | 6 | const app = new cdk.App(); 7 | new SagemakerDomainWithCodeEditorStack(app, 'SagemakerDomainWithCodeEditorStack', { 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 | 12 | /* Uncomment the next line to specialize this stack for the AWS Account 13 | * and Region that are implied by the current CLI configuration. */ 14 | env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, 15 | 16 | /* Uncomment the next line if you know exactly what Account and Region you 17 | * want to deploy the stack to. */ 18 | // env: { account: '123456789012', region: 'us-east-1' }, 19 | 20 | /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ 21 | }); -------------------------------------------------------------------------------- /code_editor/cdk.context.json: -------------------------------------------------------------------------------- 1 | { 2 | "vpc-provider:account=620878713834:filter.isDefault=true:region=us-west-2:returnAsymmetricSubnets=true": { 3 | "vpcId": "vpc-b81e21c0", 4 | "vpcCidrBlock": "172.31.0.0/16", 5 | "ownerAccountId": "620878713834", 6 | "availabilityZones": [], 7 | "subnetGroups": [ 8 | { 9 | "name": "Public", 10 | "type": "Public", 11 | "subnets": [ 12 | { 13 | "subnetId": "subnet-6249fc28", 14 | "cidr": "172.31.32.0/20", 15 | "availabilityZone": "us-west-2a", 16 | "routeTableId": "rtb-7659420d" 17 | }, 18 | { 19 | "subnetId": "subnet-a43fb7dc", 20 | "cidr": "172.31.16.0/20", 21 | "availabilityZone": "us-west-2b", 22 | "routeTableId": "rtb-7659420d" 23 | }, 24 | { 25 | "subnetId": "subnet-3752c86a", 26 | "cidr": "172.31.0.0/20", 27 | "availabilityZone": "us-west-2c", 28 | "routeTableId": "rtb-7659420d" 29 | }, 30 | { 31 | "subnetId": "subnet-b52e3c9e", 32 | "cidr": "172.31.48.0/20", 33 | "availabilityZone": "us-west-2d", 34 | "routeTableId": "rtb-7659420d" 35 | } 36 | ] 37 | } 38 | ] 39 | }, 40 | "availability-zones:account=620878713834:region=us-west-2": [ 41 | "us-west-2a", 42 | "us-west-2b", 43 | "us-west-2c", 44 | "us-west-2d" 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /code_editor/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/setup-sagemaker-domain.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 33 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 34 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 35 | "@aws-cdk/core:enablePartitionLiterals": true, 36 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 37 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 38 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 39 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 40 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 41 | "@aws-cdk/aws-route53-patters:useCertificate": true, 42 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 43 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 44 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 45 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 46 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 47 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 48 | "@aws-cdk/aws-redshift:columnId": true, 49 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 50 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 51 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 52 | "@aws-cdk/aws-kms:aliasNameRef": true, 53 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 54 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 55 | "@aws-cdk/aws-efs:denyAnonymousAccess": true, 56 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, 57 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, 58 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, 59 | "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, 60 | "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, 61 | "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, 62 | "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, 63 | "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, 64 | "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, 65 | "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, 66 | "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, 67 | "@aws-cdk/aws-eks:nodegroupNameAttribute": true, 68 | "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, 69 | "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, 70 | "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /code_editor/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 | -------------------------------------------------------------------------------- /code_editor/lib/setup-sagemaker-domain-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | import * as ec2 from "aws-cdk-lib/aws-ec2"; 4 | import * as sagemaker from "aws-cdk-lib/aws-sagemaker"; 5 | import * as iam from "aws-cdk-lib/aws-iam"; 6 | import * as fs from "fs"; 7 | import { Buffer } from "buffer"; 8 | import * as path from "path"; 9 | 10 | export class SagemakerDomainWithCodeEditorStack extends cdk.Stack { 11 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 12 | super(scope, id, props); 13 | 14 | // Load and setup a lifecycle configuration script to install docker on CodeEditor spaces. 15 | const script = fs.readFileSync( 16 | path.join(__dirname, "../scripts/install-docker-lcc-script.sh"), 17 | "utf8" 18 | ); 19 | const base64Script = Buffer.from(script, "utf-8").toString("base64"); 20 | 21 | // https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.CfnResource.html 22 | const lifecycleConfigName = "lifecycle-config-to-setup-docker"; 23 | const lifecycleConfigArn = `arn:aws:sagemaker:${this.region}:${this.account}:studio-lifecycle-config/${lifecycleConfigName}`; 24 | const lifecycleConfig = new cdk.CfnResource(this, "DockerSetupLCC", { 25 | type: "AWS::SageMaker::StudioLifecycleConfig", 26 | properties: { 27 | StudioLifecycleConfigAppType: "CodeEditor", 28 | StudioLifecycleConfigName: lifecycleConfigName, 29 | StudioLifecycleConfigContent: base64Script, 30 | }, 31 | }); 32 | 33 | // const lifecycleConfig = new sagemaker.CfnStudioLifecycleConfig(this, 'DockerSetupLCC', { 34 | // studioLifecycleConfigAppType: 'CodeEditor', 35 | // studioLifecycleConfigName: lifecycleConfigName, 36 | // studioLifecycleConfigContent: base64Script 37 | // }); 38 | 39 | // import existing default VPC 40 | const existingDefaultVpc = ec2.Vpc.fromLookup(this, "ExistingDefaultVPC", { 41 | isDefault: true, 42 | }); 43 | 44 | // Create a SageMaker execution role 45 | const sagemakerExecutionRole = new iam.Role( 46 | this, 47 | "SageMakerExecutionRole", 48 | { 49 | assumedBy: new iam.ServicePrincipal("sagemaker.amazonaws.com"), 50 | managedPolicies: [ 51 | iam.ManagedPolicy.fromAwsManagedPolicyName( 52 | "AmazonSageMakerFullAccess" 53 | ), 54 | // required to work with CDK from vscode in SageMaker 55 | iam.ManagedPolicy.fromAwsManagedPolicyName( 56 | "AWSCloudFormationFullAccess" 57 | ), 58 | iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMReadOnlyAccess"), 59 | iam.ManagedPolicy.fromAwsManagedPolicyName( 60 | "AmazonEC2ContainerRegistryPowerUser" 61 | ), 62 | iam.ManagedPolicy.fromAwsManagedPolicyName("AWSLambda_FullAccess"), 63 | ], 64 | inlinePolicies: { 65 | AssumeDeployRole: new iam.PolicyDocument({ 66 | statements: [ 67 | new iam.PolicyStatement({ 68 | effect: iam.Effect.ALLOW, 69 | actions: ["sts:AssumeRole"], 70 | resources: [ 71 | `arn:aws:iam::${this.account}:role/cdk-*-deploy-role-${this.account}-${this.region}`, 72 | `arn:aws:iam::${this.account}:role/cdk-*-file-publishing-role-${this.account}-${this.region}`, 73 | ], 74 | }), 75 | new iam.PolicyStatement({ 76 | effect: iam.Effect.ALLOW, 77 | actions: ["amplify:ListApps", "amplify:StartJob"], 78 | resources: ["*"], 79 | }), 80 | new iam.PolicyStatement({ 81 | effect: iam.Effect.ALLOW, 82 | actions: [ 83 | "textract:StartDocumentAnalysis", 84 | "textract:GetDocumentAnalysis", 85 | ], 86 | resources: ["*"], 87 | }), 88 | new iam.PolicyStatement({ 89 | effect: iam.Effect.ALLOW, 90 | actions: ["bedrock:InvokeModel"], 91 | resources: ["*"], 92 | }), 93 | ], 94 | }), 95 | }, 96 | } 97 | ); 98 | 99 | // Create a SageMaker Domain 100 | const domain = new sagemaker.CfnDomain(this, "SageMakerDomain", { 101 | vpcId: existingDefaultVpc.vpcId, 102 | authMode: "IAM", 103 | domainName: "agentic-assistant-domain", 104 | // Use private subnets 105 | subnetIds: existingDefaultVpc.publicSubnets.map( 106 | (subnet) => subnet.subnetId 107 | ), 108 | defaultUserSettings: { 109 | executionRole: sagemakerExecutionRole.roleArn, 110 | // Attach the lifecycle config to all code editor envs and make it run by default 111 | codeEditorAppSettings: { 112 | defaultResourceSpec: { 113 | lifecycleConfigArn: lifecycleConfigArn, 114 | }, 115 | lifecycleConfigArns: [lifecycleConfigArn], 116 | }, 117 | }, 118 | domainSettings: { 119 | dockerSettings: { 120 | enableDockerAccess: "ENABLED", 121 | }, 122 | }, 123 | }); 124 | 125 | domain.addDependency(lifecycleConfig); 126 | 127 | const userProfile = new sagemaker.CfnUserProfile( 128 | this, 129 | "DefaultUserProfile", 130 | { 131 | domainId: domain.attrDomainId, 132 | userProfileName: "default-profile", 133 | } 134 | ); 135 | 136 | // Create a space of type code editor 137 | const spaceName = "agentic-assistant-code-editor"; 138 | const space = new sagemaker.CfnSpace(this, "EditorSpace", { 139 | spaceName: spaceName, 140 | domainId: domain.attrDomainId, 141 | ownershipSettings: { 142 | ownerUserProfileName: userProfile.userProfileName, 143 | }, 144 | spaceSharingSettings: { 145 | sharingType: "Private", 146 | }, 147 | spaceSettings: { 148 | appType: "CodeEditor", 149 | spaceStorageSettings: { 150 | ebsStorageSettings: { 151 | ebsVolumeSizeInGb: 30, 152 | }, 153 | }, 154 | codeEditorAppSettings: { 155 | defaultResourceSpec: { 156 | instanceType: "ml.t3.medium", 157 | }, 158 | }, 159 | }, 160 | }); 161 | // Resources a provisioned in Parallel by CloudFormation. 162 | // we need this explicit dependency to avoid cases where 163 | // the space creation is attempted before the user profile is ready. 164 | space.addDependency(userProfile); 165 | 166 | new cdk.CfnOutput(this, "SageMakerDomainOutput", { 167 | value: domain.attrDomainArn, 168 | description: "The ARN of the SageMaker Domain", 169 | }); 170 | 171 | new cdk.CfnOutput(this, "SageMakerDomainIdOutput", { 172 | value: domain.attrDomainId, 173 | description: "The ID of the SageMaker Domain", 174 | key: "SageMakerDomainID", 175 | }); 176 | 177 | new cdk.CfnOutput(this, "SpaceName", { 178 | value: spaceName, 179 | description: "Space name", 180 | key: "SpaceName", 181 | }); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /code_editor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "setup-sagemaker-domain", 3 | "version": "0.1.0", 4 | "bin": { 5 | "setup-sagemaker-domain": "bin/setup-sagemaker-domain.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@types/jest": "^29.5.12", 15 | "@types/node": "20.14.2", 16 | "aws-cdk": "^2.151.0", 17 | "jest": "^29.7.0", 18 | "ts-jest": "^29.1.4", 19 | "ts-node": "^10.9.2", 20 | "typescript": "~5.4.5" 21 | }, 22 | "dependencies": { 23 | "aws-cdk-lib": "^2.151.0", 24 | "constructs": "^10.0.0", 25 | "source-map-support": "^0.5.21" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /code_editor/scripts/install-docker-lcc-script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eux 3 | 4 | # ------------------------------------------------------------------------------ 5 | # Install docker 6 | # ------------------------------------------------------------------------------ 7 | echo "Installing docker" 8 | # Based on the following link, with refactoring to use sudo https://raw.githubusercontent.com/aws-samples/amazon-sagemaker-local-mode/main/sagemaker_studio_docker_cli_install/sagemaker-ubuntu-jammy-docker-cli-install.sh --no-check-certificate 9 | 10 | # This script is meant for Ubuntu 22.04 (Jammy Jellyfish) 11 | # If you want to use another version, set the VERSION_CODENAME environment 12 | # variable when running for another version. It also defaults the DOCKER_HOST 13 | # to the location of the socaket, but if SageMaker evolves, you can set that 14 | # environment variable. 15 | # https://github.com/aws-samples/amazon-sagemaker-local-mode/blob/main/sagemaker_studio_docker_cli_install/sagemaker-ubuntu-jammy-docker-cli-install.sh 16 | 17 | sudo -E apt-get update 18 | sudo -E apt-get install -y ca-certificates curl gnupg 19 | 20 | # Create the directory and set permissions 21 | sudo mkdir -p /etc/apt/keyrings 22 | sudo chmod -R 0755 /etc/apt/keyrings 23 | 24 | # Download and add the Docker GPG key 25 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg 26 | 27 | # Set permissions for the Docker GPG key 28 | sudo chmod a+r /etc/apt/keyrings/docker.gpg 29 | 30 | # Add the Docker repository to APT sources 31 | echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "${VERSION_CODENAME:-jammy}") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null 32 | 33 | sudo -E apt-get update 34 | 35 | # Install Docker CE CLI and Docker Compose plugin 36 | VERSION_STRING="5:20.10.24~3-0~ubuntu-${VERSION_CODENAME:-jammy}" 37 | sudo -E apt-get install -y "docker-ce-cli=$VERSION_STRING" docker-compose-plugin 38 | 39 | # Validate the Docker Client is able to access Docker Server at [unix:///docker/proxy.sock] 40 | if [ -z "${DOCKER_HOST}" ]; then 41 | export DOCKER_HOST="unix:///docker/proxy.sock" 42 | fi 43 | 44 | docker version -------------------------------------------------------------------------------- /code_editor/scripts/install-lcc-script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Get the current AWS region 4 | AWS_REGION=$(aws configure get region) 5 | if [ -z "$AWS_REGION" ]; then 6 | echo "Error: Unable to determine the current AWS region. Please set the AWS_REGION environment variable." 7 | exit 1 8 | fi 9 | 10 | echo "Using AWS Region: $AWS_REGION" 11 | 12 | LCC_CONTENT=`openssl base64 -A -in install-docker-lcc-script.sh` 13 | 14 | lifecycle_config_arn=$(aws sagemaker create-studio-lifecycle-config \ 15 | --region $AWS_REGION \ 16 | --studio-lifecycle-config-name "setup-docker-lcc" \ 17 | --studio-lifecycle-config-content $LCC_CONTENT \ 18 | --studio-lifecycle-config-app-type CodeEditor) 19 | 20 | echo $lifecycle_config_arn 21 | -------------------------------------------------------------------------------- /code_editor/test/setup-sagemaker-domain.test.ts: -------------------------------------------------------------------------------- 1 | // import * as cdk from 'aws-cdk-lib'; 2 | // import { Template } from 'aws-cdk-lib/assertions'; 3 | // import * as SetupSagemakerDomain from '../lib/setup-sagemaker-domain-stack'; 4 | 5 | // example test. To run these tests, uncomment this file along with the 6 | // example resource in lib/setup-sagemaker-domain-stack.ts 7 | test('SQS Queue Created', () => { 8 | // const app = new cdk.App(); 9 | // // WHEN 10 | // const stack = new SetupSagemakerDomain.SagemakerDomainWithCodeEditorStack(app, 'MyTestStack'); 11 | // // THEN 12 | // const template = Template.fromStack(stack); 13 | 14 | // template.hasResourceProperties('AWS::SQS::Queue', { 15 | // VisibilityTimeout: 300 16 | // }); 17 | }); 18 | -------------------------------------------------------------------------------- /code_editor/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2020", 7 | "dom" 8 | ], 9 | "declaration": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": false, 19 | "inlineSourceMap": true, 20 | "inlineSources": true, 21 | "experimentalDecorators": true, 22 | "strictPropertyInitialization": false, 23 | "typeRoots": [ 24 | "./node_modules/@types" 25 | ] 26 | }, 27 | "exclude": [ 28 | "node_modules", 29 | "cdk.out" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /data_pipelines/03-document-extraction.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "ff20787a-08a8-43b0-924f-b79e625fa775", 6 | "metadata": {}, 7 | "source": [ 8 | "# 03 - Layout aware text extraction with Amazon Textract" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": null, 14 | "id": "a2321573-2b17-40dc-afcf-6075f2a8489e", 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "%pip install -q amazon-textract-textractor[pdf] pdf2image pydantic \"anthropic[bedrock]\"" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "id": "19435f95-fc3d-4769-a039-f6c48a625dbd", 25 | "metadata": {}, 26 | "outputs": [], 27 | "source": [ 28 | "!sudo apt-get update -y 2> /dev/null && sudo apt install poppler-utils -y 2> /dev/null" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": null, 34 | "id": "20bd6519-22e6-41e4-8353-632c2d68da00", 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "!ls raw_documents/" 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": null, 44 | "id": "3b72e529-3bf4-4a46-a059-d488e8b8a8bd", 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "!ls raw_documents/prepared/" 49 | ] 50 | }, 51 | { 52 | "cell_type": "code", 53 | "execution_count": null, 54 | "id": "b6713d74-77c4-4c99-a991-bdbc83585199", 55 | "metadata": {}, 56 | "outputs": [], 57 | "source": [ 58 | "!ls raw_documents/prepared/Amazon/" 59 | ] 60 | }, 61 | { 62 | "cell_type": "code", 63 | "execution_count": null, 64 | "id": "f68c5ca7-4632-4a01-8115-e285289fe1f5", 65 | "metadata": {}, 66 | "outputs": [], 67 | "source": [ 68 | "!python -m json.tool raw_documents/prepared/metadata.json" 69 | ] 70 | }, 71 | { 72 | "cell_type": "markdown", 73 | "id": "9cb3114c-e1db-46dc-8234-f98f58a135f2", 74 | "metadata": {}, 75 | "source": [ 76 | "## Extraction with textractor" 77 | ] 78 | }, 79 | { 80 | "cell_type": "code", 81 | "execution_count": null, 82 | "id": "363c90b1-b849-447f-a8c9-b64d9d655e73", 83 | "metadata": {}, 84 | "outputs": [], 85 | "source": [ 86 | "import sagemaker\n", 87 | "\n", 88 | "default_sagemaker_bucket = sagemaker.Session().default_bucket()\n", 89 | "sagemaker_execution_role = sagemaker.get_execution_role()" 90 | ] 91 | }, 92 | { 93 | "cell_type": "code", 94 | "execution_count": 11, 95 | "id": "653a51ce-5f25-48f3-a2f9-ce7964732768", 96 | "metadata": {}, 97 | "outputs": [], 98 | "source": [ 99 | "import boto3\n", 100 | "from textractor import Textractor\n", 101 | "from textractor.data.constants import TextractFeatures\n", 102 | "\n", 103 | "region = boto3.session.Session().region_name\n", 104 | "# extractor = Textractor(profile_name=\"default\")\n", 105 | "extractor = Textractor(region_name=region)\n", 106 | "\n", 107 | "input_document = \"raw_documents/prepared/Amazon/annual_report_2022.pdf\"\n", 108 | "\n", 109 | "document = extractor.start_document_analysis(\n", 110 | " file_source=input_document,\n", 111 | " s3_upload_path=f\"s3://{default_sagemaker_bucket}/input_documents/\",\n", 112 | " s3_output_path=f\"s3://{default_sagemaker_bucket}/output_documents/\",\n", 113 | " features=[TextractFeatures.LAYOUT],\n", 114 | " save_image=False\n", 115 | ")" 116 | ] 117 | }, 118 | { 119 | "cell_type": "code", 120 | "execution_count": 12, 121 | "id": "ada94249-6a6e-4bbd-a790-0d23fb4fe5c6", 122 | "metadata": {}, 123 | "outputs": [], 124 | "source": [ 125 | "document.document" 126 | ] 127 | }, 128 | { 129 | "cell_type": "code", 130 | "execution_count": null, 131 | "id": "118c3f36-30d6-4c01-8d10-0f9a30b998e2", 132 | "metadata": {}, 133 | "outputs": [], 134 | "source": [ 135 | "document.pages[0]" 136 | ] 137 | }, 138 | { 139 | "cell_type": "code", 140 | "execution_count": null, 141 | "id": "fff7a03c-b86f-49fa-b293-b2a056e2bb1f", 142 | "metadata": {}, 143 | "outputs": [], 144 | "source": [ 145 | "print(document.pages[4].to_markdown())" 146 | ] 147 | }, 148 | { 149 | "cell_type": "markdown", 150 | "id": "57f3357e-b7c5-43a3-b65c-ba40857ae080", 151 | "metadata": {}, 152 | "source": [ 153 | "## Use LLM to review and improve the extracted document\n", 154 | "\n", 155 | "Here we use Anthropic Claude 3 models through Amazon Bedrock to improve the markdown file extracted by Amazon Textract further, so it is ready for the LLM to answer question properly later on." 156 | ] 157 | }, 158 | { 159 | "cell_type": "code", 160 | "execution_count": 19, 161 | "id": "25fe312e-660a-4787-8124-02b17dbc1e60", 162 | "metadata": {}, 163 | "outputs": [], 164 | "source": [ 165 | "import boto3\n", 166 | "import json\n", 167 | "import logging\n", 168 | "from botocore.exceptions import ClientError\n", 169 | "\n", 170 | "bedrock = boto3.client(\"bedrock\", region_name=\"us-west-2\")\n", 171 | "bedrock_runtime = boto3.client(\"bedrock-runtime\", region_name=\"us-west-2\")" 172 | ] 173 | }, 174 | { 175 | "cell_type": "code", 176 | "execution_count": 20, 177 | "id": "9e94a2ff-be70-4786-a61f-f6622ddbde4c", 178 | "metadata": {}, 179 | "outputs": [], 180 | "source": [ 181 | "# bedrock.list_foundation_models()" 182 | ] 183 | }, 184 | { 185 | "cell_type": "code", 186 | "execution_count": 21, 187 | "id": "2b33773e-5fb2-4411-90d1-5bfbc726b4fa", 188 | "metadata": {}, 189 | "outputs": [], 190 | "source": [ 191 | "# llm_model_id = \"anthropic.claude-3-haiku-20240307-v1:0\"\n", 192 | "llm_model_id = \"anthropic.claude-3-sonnet-20240229-v1:0\"" 193 | ] 194 | }, 195 | { 196 | "cell_type": "code", 197 | "execution_count": 22, 198 | "id": "f638ca66-f904-46c0-b72d-809154f9f4ab", 199 | "metadata": {}, 200 | "outputs": [], 201 | "source": [ 202 | "logger = logging.getLogger(__name__)\n", 203 | "logging.basicConfig(level=logging.INFO)\n", 204 | "\n", 205 | "\n", 206 | "def generate_message(bedrock_runtime, model_id, system_prompt, messages, max_tokens):\n", 207 | "\n", 208 | " body=json.dumps(\n", 209 | " {\n", 210 | " \"anthropic_version\": \"bedrock-2023-05-31\",\n", 211 | " \"max_tokens\": max_tokens,\n", 212 | " \"system\": system_prompt,\n", 213 | " \"messages\": messages\n", 214 | " }\n", 215 | " )\n", 216 | " response = bedrock_runtime.invoke_model(body=body, modelId=model_id)\n", 217 | " response_body = json.loads(response.get('body').read())\n", 218 | "\n", 219 | " return response_body\n", 220 | "\n", 221 | "\n", 222 | "def call_llm(user_input, model_id, system_prompt, bedrock_runtime, max_tokens=1000):\n", 223 | " \"\"\"Handle calls to Anthropic Claude message api.\"\"\"\n", 224 | " try:\n", 225 | " # Prompt with user turn only.\n", 226 | " user_message = {\"role\": \"user\", \"content\": user_input}\n", 227 | " messages = [user_message]\n", 228 | " return generate_message(bedrock_runtime, model_id, system_prompt, messages, max_tokens)\n", 229 | " except ClientError as err:\n", 230 | " message=err.response[\"Error\"][\"Message\"]\n", 231 | " logger.error(\"A client error occurred: %s\", message)\n", 232 | " print(\"A client error occured: \" +\n", 233 | " format(message))\n", 234 | "\n" 235 | ] 236 | }, 237 | { 238 | "cell_type": "markdown", 239 | "id": "cc681f37-82e9-4878-86cc-760122218b5f", 240 | "metadata": {}, 241 | "source": [ 242 | "Below we test the help functions by calling the LLM" 243 | ] 244 | }, 245 | { 246 | "cell_type": "code", 247 | "execution_count": null, 248 | "id": "5cae7812-d193-4e39-8a3e-a29884fef932", 249 | "metadata": {}, 250 | "outputs": [], 251 | "source": [ 252 | "%%time\n", 253 | "user_input = \"hello\"\n", 254 | "system_prompt = \"reply in a friendly manner\"\n", 255 | "\n", 256 | "call_llm(user_input, llm_model_id, system_prompt, bedrock_runtime, max_tokens=1000)" 257 | ] 258 | }, 259 | { 260 | "cell_type": "code", 261 | "execution_count": 24, 262 | "id": "71d0306d-08fa-4b49-8ad3-05a0d97f0174", 263 | "metadata": {}, 264 | "outputs": [], 265 | "source": [ 266 | "user_prompt = \"\"\"\n", 267 | "Improve the markdown while keeping all original information. Put the improved markdown inside a xml tags with no explanation:\n", 268 | "\\n{markdown_doc}\n", 269 | "\"\"\".strip()\n", 270 | "\n", 271 | "system_prompt = \"Your task is to review and improve the results of Amazon textract in markdown.\"\n", 272 | "\n", 273 | "\n", 274 | "def improve_textract_markdown_output(document, llm_model_id):\n", 275 | " improved_markdown = []\n", 276 | " for i in range(len(document.pages)):\n", 277 | " user_input = user_prompt.format(markdown_doc=document.pages[i].to_markdown())\n", 278 | " result = call_llm(user_input, llm_model_id, system_prompt, bedrock_runtime, max_tokens=3000)\n", 279 | " # Extract the text between the XML tags only.\n", 280 | " improved_markdown.append(result[\"content\"][0][\"text\"].split(\"\")[-1].split(\"\")[0].strip())\n", 281 | " return improved_markdown" 282 | ] 283 | }, 284 | { 285 | "cell_type": "code", 286 | "execution_count": null, 287 | "id": "c61b27b1-0fab-4456-801a-5ef7458d5629", 288 | "metadata": {}, 289 | "outputs": [], 290 | "source": [ 291 | "import os\n", 292 | "raw_base_directory = \"raw_documents\"\n", 293 | "prepared_base_directory = os.path.join(raw_base_directory, \"prepared/\")\n", 294 | "prepared_base_directory" 295 | ] 296 | }, 297 | { 298 | "cell_type": "code", 299 | "execution_count": 27, 300 | "id": "da319b54-e2a4-4c63-8955-7b72d81298be", 301 | "metadata": {}, 302 | "outputs": [], 303 | "source": [ 304 | "import json\n", 305 | "\n", 306 | "with open(\n", 307 | " os.path.join(prepared_base_directory, \"metadata.json\"), \"r\"\n", 308 | ") as prepared_pdfs_metadata_obj:\n", 309 | " prepared_pdfs_metadata = json.load(prepared_pdfs_metadata_obj)\n" 310 | ] 311 | }, 312 | { 313 | "cell_type": "code", 314 | "execution_count": null, 315 | "id": "14f40cf8-d02b-4fbf-b334-dd0cf3754c23", 316 | "metadata": {}, 317 | "outputs": [], 318 | "source": [ 319 | "prepared_pdfs_metadata" 320 | ] 321 | }, 322 | { 323 | "cell_type": "code", 324 | "execution_count": 29, 325 | "id": "46f8e0ed-65fd-4a1d-9cba-ddb5846d6822", 326 | "metadata": {}, 327 | "outputs": [], 328 | "source": [ 329 | "def extract_pages_as_markdown(input_document):\n", 330 | "\n", 331 | " document = extractor.start_document_analysis(\n", 332 | " file_source=input_document,\n", 333 | " s3_upload_path=f\"s3://{default_sagemaker_bucket}/input_documents/\",\n", 334 | " s3_output_path=f\"s3://{default_sagemaker_bucket}/output_documents/\",\n", 335 | " features=[TextractFeatures.LAYOUT],\n", 336 | " save_image=False\n", 337 | " )\n", 338 | "\n", 339 | " res = improve_textract_markdown_output(document, llm_model_id)\n", 340 | " pages = [{\"page\": indx, \"page_text\": text} for indx, text in enumerate(res)]\n", 341 | " return pages\n", 342 | "\n", 343 | "\n", 344 | "def extract_docs_into_markdown(docs_metadata):\n", 345 | " results = []\n", 346 | " for doc_meta in docs_metadata:\n", 347 | " doc_result_with_metadata = {}\n", 348 | " doc_result_with_metadata[\"metadata\"] = doc_meta\n", 349 | " doc_result_with_metadata[\"name\"] = doc_meta[\"doc_url\"].split(\"/\")[-1]\n", 350 | " doc_result_with_metadata[\"source_location\"] = doc_meta[\"doc_url\"]\n", 351 | " doc_result_with_metadata[\"pages\"] = extract_pages_as_markdown(doc_meta[\"local_pdf_path\"])\n", 352 | " results.append(doc_result_with_metadata)\n", 353 | " return results" 354 | ] 355 | }, 356 | { 357 | "cell_type": "code", 358 | "execution_count": null, 359 | "id": "e37c8a74-a33f-4d7f-8b0f-400d7c8984b1", 360 | "metadata": {}, 361 | "outputs": [], 362 | "source": [ 363 | "%%time\n", 364 | "results = extract_docs_into_markdown(prepared_pdfs_metadata)" 365 | ] 366 | }, 367 | { 368 | "cell_type": "code", 369 | "execution_count": null, 370 | "id": "bc6f950d-af99-4859-b6dc-94e5d541de26", 371 | "metadata": {}, 372 | "outputs": [], 373 | "source": [ 374 | "results[0]" 375 | ] 376 | }, 377 | { 378 | "cell_type": "code", 379 | "execution_count": 32, 380 | "id": "1199a24c-13db-4742-a95a-96290481d371", 381 | "metadata": {}, 382 | "outputs": [], 383 | "source": [ 384 | "from utils.helpers import store_list_to_s3\n", 385 | "ssm = boto3.client(\"ssm\")" 386 | ] 387 | }, 388 | { 389 | "cell_type": "code", 390 | "execution_count": 34, 391 | "id": "2c66762e-514c-422d-a4f9-cb851af9892e", 392 | "metadata": {}, 393 | "outputs": [], 394 | "source": [ 395 | "s3_bucket_name_parameter = \"/AgenticLLMAssistantWorkshop/AgentDataBucketParameter\"\n", 396 | "s3_bucket_name = ssm.get_parameter(Name=s3_bucket_name_parameter)\n", 397 | "s3_bucket_name = s3_bucket_name[\"Parameter\"][\"Value\"]\n", 398 | "processed_documents_s3_key = \"documents_processed.json\"" 399 | ] 400 | }, 401 | { 402 | "cell_type": "code", 403 | "execution_count": 37, 404 | "id": "1b6cc0b7-0abe-4f49-9282-ccce09fd70e8", 405 | "metadata": {}, 406 | "outputs": [], 407 | "source": [ 408 | "store_list_to_s3(s3_bucket_name, processed_documents_s3_key, results)" 409 | ] 410 | } 411 | ], 412 | "metadata": { 413 | "kernelspec": { 414 | "display_name": "Python 3 (ipykernel)", 415 | "language": "python", 416 | "name": "python3" 417 | }, 418 | "language_info": { 419 | "codemirror_mode": { 420 | "name": "ipython", 421 | "version": 3 422 | }, 423 | "file_extension": ".py", 424 | "mimetype": "text/x-python", 425 | "name": "python", 426 | "nbconvert_exporter": "python", 427 | "pygments_lexer": "ipython3", 428 | "version": "3.10.13" 429 | } 430 | }, 431 | "nbformat": 4, 432 | "nbformat_minor": 5 433 | } 434 | -------------------------------------------------------------------------------- /data_pipelines/README.md: -------------------------------------------------------------------------------- 1 | We define the following SageMaker notebooks: 2 | 3 | - `01-validate-sagemaker-jobs-connection-to-postgreSQL.ipynb`: Defines a SageMaker Processing Job that determines whether you can connect to your PostgreSQL database from within a SageMaker Processing Job. This is a test that you can run before moving on to the next jobs. 4 | - `02-download-raw-pdf-documents.ipynb`: Downloads the raw PDF documents from a specified source and stores them in an S3 bucket. 5 | - `03-document-extraction.ipynb`: Extracts text and metadata from the raw PDF documents and stores the processed data in a JSON file. 6 | - `04-create-and-load-embeddings-into-aurora-postgreSQL.ipynb`: Defines a SageMaker Processing Job which, when provided with the extracted text from your PDF documents as an input, embeds this text using Bedrock and creates a vector store in the PostgreSQL database that can then be used as a tool by the AI agent hosted in AWS Lambda and accessible through Streamlit. By default, the script uses the processed documents in the `documents_processed.json` file at the root level of the S3 bucket, which is created when you deploy the solution. 7 | - `05-load-sql-tables-into-aurora-postgreSQL.ipynb`: Defines a SageMaker Processing Job which, when provided with structured metadata containing the extracted entities, loads this metadata into the PostgreSQL database such that it can be used by the AI agent to answer questions involving the metadata. 8 | - `06-sagemaker-pipeline-for-documents-processing.ipynb`: Defines a SageMaker Pipeline which contains the SageMaker Processing Jobs from `04-create-and-load-embeddings-into-aurora-postgreSQL` and `05-load-sql-tables-into-aurora-postgreSQL` as steps, essentially allowing you to update your agent with the latest data from S3 in a single click. 9 | 10 | The data processing pipeline consists of the following steps: 11 | 12 | 1. Download raw PDF documents using `02-download-raw-pdf-documents.ipynb`. 13 | 2. Extract text and metadata from the raw PDF documents using `03-document-extraction.ipynb`. 14 | 3. Create and load embeddings into the PostgreSQL database using `04-create-and-load-embeddings-into-aurora-postgreSQL.ipynb`. 15 | 4. Load structured metadata into SQL tables in the PostgreSQL database using `05-load-sql-tables-into-aurora-postgreSQL.ipynb`. 16 | 5. (Optional) Run the SageMaker Pipeline defined in `06-sagemaker-pipeline-for-documents-processing.ipynb` to update the data in a single click. 17 | -------------------------------------------------------------------------------- /data_pipelines/data/extracted_entities.csv: -------------------------------------------------------------------------------- 1 | ,company,year,source_doc,revenue,revenue_reasoning,revenue_unit,revenue_unit_reasoning,risks,risks_reasoning,human_capital,human_capital_reasoning 2 | 0,Amazon,2022,s3:///prepared_pdf_documents/Amazon/annual_report_2022.pdf,513983000000.0,"Total net sales for Amazon in 2022 was $513,983 million.",USD,The financial report is in US dollars.,"The main risks impacting Amazon in 2022 are: competition from new business models and well-funded competitors, competitors forming alliances, established companies expanding into Amazon's segments, and new technologies enabling increased competition. Other risks include limited experience and lack of first-mover advantage in new segments, failure of new products/services to be adopted, disruption risks from new products/services, lack of profitability in new segments, and unsuccessful sustainability initiatives.","Our businesses are rapidly evolving and intensely competitive, and we have many competitors across geographies, including cross-border competition, and in different industries, including physical, e-commerce, and omnichannel retail, e-commerce services, web and infrastructure computing services, electronic devices, digital content, advertising, grocery, and transportation and logistics services. Some of our current and potential competitors have greater resources, longer histories, more customers, and/or greater brand recognition, particularly with our newly-launched products and services and in our newer geographic regions. They may secure better terms from vendors, adopt more aggressive pricing, and devote more resources to technology, infrastructure, fulfillment, and marketing. Competition continues to intensify, including with the development of new business models and the entry of new and well-funded competitors, and as our competitors enter into business combinations or alliances and established companies in other market segments expand to become competitive with our business. In addition, new and enhanced technologies, including search, web and infrastructure computing services, digital content, and electronic devices continue to increase our competition. We may have limited or no experience in our newer market segments, and our customers may not adopt our product or service offerings. These offerings, which can present new and difficult technology challenges, may subject us to claims if customers of these offerings experience, or are otherwise impacted by, service disruptions, delays, setbacks, or failures or quality issues. In addition, profitability, if any, in our newer activities may not meet our expectations, and we may not be successful enough in these newer activities to recoup our investments in them, which investments are often significant. Failure to realize the benefits of amounts we invest in new technologies, products, or services could result in the value of those investments being written down or written off. In addition, our sustainability initiatives may be unsuccessful for a variety of reasons, including if we are unable to realize the expected benefits of new technologies or if we do not successfully plan or execute new strategies, which could harm our business or damage our reputation.",1541000,"As of December 31, 2022, we employed approximately 1,541,000 full-time and part-time employees." 3 | 1,Amazon,2021,s3:///prepared_pdf_documents/Amazon/annual_report_2021.pdf,469822000000.0,"Total net sales in 2021 was $469,822 million according to the table on page 47.",USD,The financial report is in US dollars.,"The main risks impacting Amazon in 2021 are: intense competition, expansion into new products and geographies, dependence on technology, seasonal fluctuations in business, fraudulent activities by sellers, protecting intellectual property, and compliance with regulations in international markets.","Competition continues to intensify, including with the development of new business models and the entry of new and well-funded competitors, and as our competitors enter into business combinations or alliances and established companies in other market segments expand to become competitive with our business. In addition, new and enhanced technologies, including search, web and infrastructure computing services, digital content, and electronic devices continue to increase our competition. Our Expansion into New Products, Services, Technologies, and Geographic Regions Subjects Us to Additional Risks. Our International Operations Expose Us to a Number of Risks.",1608000,"As of December 31, 2021, we employed approximately 1,608,000 full-time and part-time employees." 4 | -------------------------------------------------------------------------------- /data_pipelines/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim 2 | 3 | COPY requirements.txt . 4 | RUN pip3 install -r requirements.txt 5 | 6 | ENV PYTHONUNBUFFERED=TRUE 7 | ENTRYPOINT ["python3"] -------------------------------------------------------------------------------- /data_pipelines/docker/requirements.txt: -------------------------------------------------------------------------------- 1 | # The specified boto versions recognize Amazon Bedrock service after general availability. 2 | boto3>=1.28.57 3 | botocore>=1.31.57 4 | pandas==2.0.3 5 | dask[dataframe]==2024.10.0 6 | tabulate 7 | langchain==0.2.12 8 | langchain-aws==0.1.16 9 | langchain-community==0.2.11 10 | psycopg2-binary==2.9.9 11 | # tiktoken is required by langchain.text_splitter.TokenTextSplitter 12 | tiktoken 13 | pgvector==0.2.5 14 | sqlalchemy==2.0.30 -------------------------------------------------------------------------------- /data_pipelines/scripts/check_connection_to_aurora_postgres.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | import boto3 5 | import psycopg2 6 | 7 | secretsmanager = boto3.client("secretsmanager") 8 | 9 | secret_response = secretsmanager.get_secret_value( 10 | SecretId=os.environ["SQL_DB_SECRET_ID"] 11 | ) 12 | 13 | database_secrets = json.loads(secret_response["SecretString"]) 14 | 15 | # Extract credentials 16 | host = database_secrets['host'] 17 | dbname = database_secrets['dbname'] 18 | username = database_secrets['username'] 19 | password = database_secrets['password'] 20 | 21 | 22 | def test_db_connection(): 23 | # Connect to the database 24 | conn = psycopg2.connect( 25 | host=host, 26 | database=dbname, 27 | user=username, 28 | password=password 29 | ) 30 | # Get cursor 31 | cur = conn.cursor() 32 | 33 | # Query to get all tables 34 | cur.execute("SELECT table_name FROM information_schema.tables WHERE table_schema='public';") 35 | 36 | # Fetch all the tables 37 | tables = cur.fetchall() 38 | 39 | # Print the table names 40 | print(f"SQL tables: {tables}") 41 | 42 | # Close connection 43 | conn.close() 44 | 45 | 46 | if __name__ == "__main__": 47 | test_db_connection() 48 | -------------------------------------------------------------------------------- /data_pipelines/scripts/load_sql_tables.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | import boto3 5 | import dask.dataframe as dd 6 | import psycopg2 7 | import sqlalchemy 8 | 9 | secretsmanager = boto3.client("secretsmanager") 10 | 11 | secret_response = secretsmanager.get_secret_value( 12 | SecretId=os.environ["SQL_DB_SECRET_ID"] 13 | ) 14 | 15 | database_secrets = json.loads(secret_response["SecretString"]) 16 | 17 | # Extract credentials 18 | host = database_secrets['host'] 19 | dbname = database_secrets['dbname'] 20 | username = database_secrets['username'] 21 | password = database_secrets['password'] 22 | port = database_secrets["port"] 23 | 24 | db_connection = psycopg2.connect( 25 | host=host, 26 | port=port, 27 | database=dbname, 28 | user=username, 29 | password=password, 30 | ) 31 | 32 | 33 | def activate_vector_extension(db_connection): 34 | """Activate PGVector extension.""" 35 | 36 | db_connection.autocommit = True 37 | cursor = db_connection.cursor() 38 | # install pgvector 39 | cursor.execute("CREATE EXTENSION IF NOT EXISTS vector;") 40 | db_connection.close() 41 | 42 | 43 | def test_db_connection(): 44 | # Connect to the database 45 | conn = psycopg2.connect( 46 | host=host, 47 | database=dbname, 48 | user=username, 49 | password=password 50 | ) 51 | # Get cursor 52 | cur = conn.cursor() 53 | 54 | # Query to get all tables 55 | cur.execute("SELECT table_name FROM information_schema.tables WHERE table_schema='public';") 56 | 57 | # Fetch all the tables 58 | tables = cur.fetchall() 59 | 60 | # Print the table names 61 | print(f"SQL tables: {tables}") 62 | 63 | # Close connection 64 | conn.close() 65 | 66 | 67 | def load_sql_tables(raw_tables_base_path, raw_tables_data_paths, columns_to_load, engine): 68 | """Load csv files as SQL tables into an Amazon Aurora PostgreSQL DB. 69 | 70 | Note: 71 | raw_tables_data_paths (List, str): a list of strings, each string 72 | can be a csv file, or a folder that contains a partitioned csv file. 73 | """ 74 | 75 | for raw_table_path in raw_tables_data_paths: 76 | data_loading_path = os.path.join( 77 | raw_tables_base_path, 78 | raw_table_path 79 | ) 80 | 81 | if os.path.isdir(data_loading_path): 82 | data_loading_path = os.path.join(data_loading_path, "*") 83 | table_name = raw_table_path 84 | else: 85 | table_name = raw_table_path.split(".")[0] 86 | 87 | print(f"Loading {table_name} data into a pandas dataframe") 88 | current_data_df = dd.read_csv(data_loading_path).compute() 89 | if columns_to_load == "all": 90 | columns_to_load = current_data_df.columns 91 | 92 | current_data_df = current_data_df[columns_to_load] 93 | 94 | current_data_df.to_sql( 95 | table_name, engine, if_exists='replace', index=False 96 | ) 97 | 98 | return True 99 | 100 | 101 | if __name__ == "__main__": 102 | test_db_connection() 103 | 104 | url_object = sqlalchemy.URL.create( 105 | "postgresql+psycopg2", 106 | username=username, 107 | password=password, 108 | host=host, 109 | database=dbname, 110 | ) 111 | 112 | db_engine = sqlalchemy.create_engine(url_object) 113 | 114 | input_data_base_path = "/opt/ml/processing/input/" 115 | raw_sql_tables_base_path = os.path.join(input_data_base_path, "sqltables") 116 | tables_raw_data_paths = os.listdir(raw_sql_tables_base_path) 117 | columns_to_load = "all" 118 | 119 | print(raw_sql_tables_base_path, tables_raw_data_paths) 120 | load_sql_tables( 121 | raw_sql_tables_base_path, 122 | tables_raw_data_paths, 123 | columns_to_load, 124 | db_engine 125 | ) 126 | 127 | test_db_connection() 128 | -------------------------------------------------------------------------------- /data_pipelines/scripts/prepare_and_load_data_in_aurora.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from botocore.config import Config 4 | import boto3 5 | import dask.dataframe as dd 6 | from langchain_aws import BedrockEmbeddings 7 | from langchain.schema.document import Document 8 | from langchain.text_splitter import TokenTextSplitter 9 | from langchain_community.vectorstores.pgvector import PGVector 10 | 11 | import psycopg2 12 | import sqlalchemy 13 | 14 | ssm = boto3.client("ssm") 15 | 16 | secretsmanager = boto3.client("secretsmanager") 17 | secret_response = secretsmanager.get_secret_value( 18 | SecretId=os.environ["SQL_DB_SECRET_ID"] 19 | ) 20 | database_secrets = json.loads(secret_response["SecretString"]) 21 | 22 | # Extract credentials 23 | host = database_secrets['host'] 24 | dbname = database_secrets['dbname'] 25 | username = database_secrets['username'] 26 | password = database_secrets['password'] 27 | port = database_secrets["port"] 28 | 29 | CONNECTION_STRING = PGVector.connection_string_from_db_params( 30 | driver="psycopg2", 31 | host=host, 32 | port=port, 33 | database=dbname, 34 | user=username, 35 | password=password, 36 | ) 37 | 38 | db_connection = psycopg2.connect( 39 | host=host, 40 | port=port, 41 | database=dbname, 42 | user=username, 43 | password=password, 44 | ) 45 | 46 | BEDROCK_CROSS_ACCOUNT_ROLE_ARN = os.environ.get("BEDROCK_CROSS_ACCOUNT_ROLE_ARN") 47 | bedrock_region_parameter = "/AgenticLLMAssistantWorkshop/bedrock_region" 48 | 49 | BEDROCK_REGION = ssm.get_parameter(Name=bedrock_region_parameter) 50 | BEDROCK_REGION = BEDROCK_REGION["Parameter"]["Value"] 51 | 52 | retry_config = Config( 53 | region_name=BEDROCK_REGION, 54 | retries={"max_attempts": 10, "mode": "standard"} 55 | ) 56 | bedrock_runtime = boto3.client("bedrock-runtime", config=retry_config) 57 | bedrock = boto3.client("bedrock", config=retry_config) 58 | 59 | 60 | def activate_vector_extension(db_connection): 61 | """Activate PGVector extension.""" 62 | 63 | db_connection.autocommit = True 64 | cursor = db_connection.cursor() 65 | # install pgvector 66 | cursor.execute("CREATE EXTENSION IF NOT EXISTS vector;") 67 | db_connection.close() 68 | 69 | 70 | def test_db_connection(): 71 | # Connect to the database 72 | conn = psycopg2.connect( 73 | host=host, 74 | database=dbname, 75 | user=username, 76 | password=password 77 | ) 78 | # Get cursor 79 | cur = conn.cursor() 80 | 81 | # Query to get all tables 82 | cur.execute("SELECT table_name FROM information_schema.tables WHERE table_schema='public';") 83 | 84 | # Fetch all the tables 85 | tables = cur.fetchall() 86 | 87 | # Print the table names 88 | print(f"SQL tables: {tables}") 89 | 90 | # Close connection 91 | conn.close() 92 | 93 | 94 | def load_sql_tables(raw_tables_base_path, raw_tables_data_paths, columns_to_load, engine): 95 | """Load csv files as SQL tables into an Amazon Aurora PostgreSQL DB. 96 | 97 | Note: 98 | raw_tables_data_paths (List, str): a list of strings, each string 99 | can be a csv file, or a folder that contains a partitioned csv file. 100 | """ 101 | 102 | for raw_table_path in raw_tables_data_paths: 103 | data_loading_path = os.path.join( 104 | raw_tables_base_path, 105 | raw_table_path 106 | ) 107 | 108 | if os.path.isdir(data_loading_path): 109 | data_loading_path = os.path.join(data_loading_path, "*") 110 | table_name = raw_table_path 111 | else: 112 | table_name = raw_table_path.split(".")[0] 113 | 114 | print(f"Loading {table_name} data into a pandas dataframe") 115 | current_data_df = dd.read_csv(data_loading_path).compute() 116 | if columns_to_load == "all": 117 | columns_to_load = current_data_df.columns 118 | 119 | current_data_df = current_data_df[columns_to_load] 120 | 121 | current_data_df.to_sql( 122 | table_name, engine, if_exists='replace', index=False 123 | ) 124 | 125 | return True 126 | 127 | 128 | def prepare_documents_with_metadata(documents_processed): 129 | 130 | langchain_documents_text = [] 131 | langchain_documents_tables = [] 132 | 133 | for document in documents_processed: 134 | document_name = document['name'] 135 | document_source_location = document['source_location'] 136 | document_s3_metadata = document['metadata'] 137 | 138 | mapping_to_original_page_numbers = { 139 | idx: pg_num for idx, pg_num 140 | in enumerate(document_s3_metadata["pages_kept"]) 141 | } 142 | # remove pages_kept since we already put the original page number. 143 | del document_s3_metadata["pages_kept"] 144 | 145 | for page in document['pages']: 146 | # Turn each page into a Langchain Document. 147 | # Note: you could choose to also prepend part of the previous document 148 | # and append part of the next document to include more context for documents 149 | # that have many pages which continue their text on the next page. 150 | current_metadata = { 151 | 'document_name': document_name, 152 | 'document_source_location': document_source_location, 153 | 'page_number': page['page'], 154 | 'original_page_number': mapping_to_original_page_numbers[page['page']] 155 | } 156 | # merge the document_s3_metadata into the langchain Document metadata 157 | # to be able to use them for filtering. 158 | current_metadata.update(document_s3_metadata) 159 | 160 | langchain_documents_text.append( 161 | Document( 162 | page_content=page['page_text'], 163 | metadata=current_metadata 164 | ) 165 | ) 166 | # Turn all the tables of the pages into seperate Langchain Documents as well 167 | # for table in page['page_tables']: 168 | # langchain_documents_tables.append( 169 | # Document( 170 | # page_content=table, 171 | # metadata=current_metadata 172 | # ) 173 | # ) 174 | 175 | # return langchain_documents_text, langchain_documents_tables 176 | return langchain_documents_text 177 | 178 | 179 | def load_processed_documents(json_file_path): 180 | with open(json_file_path, 'rb') as file: 181 | processed_documents = json.load(file) 182 | return processed_documents 183 | 184 | 185 | if __name__ == "__main__": 186 | print("Start") 187 | print("Test connection to db...", end="") 188 | test_db_connection() 189 | print("success!") 190 | 191 | url_object = sqlalchemy.URL.create( 192 | "postgresql+psycopg2", 193 | username=username, 194 | password=password, 195 | host=host, 196 | database=dbname, 197 | ) 198 | 199 | input_data_base_path = "/opt/ml/processing/input/" 200 | processed_docs_filename = "documents_processed.json" 201 | token_split_chunk_size = 512 202 | token_chunk_overlap = 64 203 | # Define an embedding model to generate embeddings 204 | embedding_model_id = "amazon.titan-embed-text-v1" 205 | COLLECTION_NAME = 'agentic_assistant_vector_store' 206 | # make this an argument. 207 | pre_delete_collection = True 208 | 209 | db_engine = sqlalchemy.create_engine(url_object) 210 | 211 | processed_documents_file_path = os.path.join( 212 | input_data_base_path, 213 | "processed_documents", 214 | processed_docs_filename 215 | ) 216 | 217 | print(processed_documents_file_path) 218 | 219 | if os.path.isfile(processed_documents_file_path): 220 | processed_documents = load_processed_documents(processed_documents_file_path) 221 | langchain_documents_text = prepare_documents_with_metadata( 222 | processed_documents 223 | ) 224 | # The chunk overlap duplicates some text across chunks 225 | # to prevent context from being lost between chunks. 226 | # TODO: the following spliting uses tiktoken, 227 | # create a custom one that use the tokenizer from anthropic. 228 | text_splitter = TokenTextSplitter( 229 | chunk_size=token_split_chunk_size, 230 | chunk_overlap=token_chunk_overlap 231 | ) 232 | 233 | langchain_documents_text_chunked = text_splitter.split_documents( 234 | langchain_documents_text 235 | ) 236 | 237 | embedding_model = BedrockEmbeddings( 238 | model_id=embedding_model_id, 239 | client=bedrock_runtime 240 | ) 241 | 242 | activate_vector_extension(db_connection) 243 | 244 | pgvector_store = PGVector( 245 | collection_name=COLLECTION_NAME, 246 | connection_string=CONNECTION_STRING, 247 | embedding_function=embedding_model, 248 | pre_delete_collection=pre_delete_collection 249 | ) 250 | 251 | pgvector_store.add_documents(langchain_documents_text_chunked) 252 | 253 | print("test indexing results") 254 | test_question = "Who were in the board of directors of Amazon in 2021 and what were their positions?" 255 | print(pgvector_store.similarity_search_with_score(test_question)) 256 | 257 | else: 258 | raise ValueError(f"{processed_documents_file_path} must be a file.") 259 | 260 | input_data_base_path = "/opt/ml/processing/input/" 261 | raw_sql_tables_base_path = os.path.join(input_data_base_path, "sqltables") 262 | tables_raw_data_paths = os.listdir(raw_sql_tables_base_path) 263 | columns_to_load = "all" 264 | 265 | print(raw_sql_tables_base_path, tables_raw_data_paths) 266 | load_sql_tables( 267 | raw_sql_tables_base_path, 268 | tables_raw_data_paths, 269 | columns_to_load, 270 | db_engine 271 | ) 272 | 273 | test_db_connection() 274 | -------------------------------------------------------------------------------- /data_pipelines/scripts/prepare_and_load_embeddings.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from botocore.config import Config 4 | import boto3 5 | from langchain.embeddings import BedrockEmbeddings 6 | from langchain.schema.document import Document 7 | from langchain.text_splitter import TokenTextSplitter 8 | from langchain.vectorstores.pgvector import PGVector 9 | 10 | import psycopg2 11 | import sqlalchemy 12 | 13 | ssm = boto3.client("ssm") 14 | 15 | secretsmanager = boto3.client("secretsmanager") 16 | secret_response = secretsmanager.get_secret_value( 17 | SecretId=os.environ["SQL_DB_SECRET_ID"] 18 | ) 19 | database_secrets = json.loads(secret_response["SecretString"]) 20 | 21 | # Extract credentials 22 | host = database_secrets['host'] 23 | dbname = database_secrets['dbname'] 24 | username = database_secrets['username'] 25 | password = database_secrets['password'] 26 | port = database_secrets["port"] 27 | 28 | CONNECTION_STRING = PGVector.connection_string_from_db_params( 29 | driver="psycopg2", 30 | host=host, 31 | port=port, 32 | database=dbname, 33 | user=username, 34 | password=password, 35 | ) 36 | 37 | db_connection = psycopg2.connect( 38 | host=host, 39 | port=port, 40 | database=dbname, 41 | user=username, 42 | password=password, 43 | ) 44 | 45 | BEDROCK_CROSS_ACCOUNT_ROLE_ARN = os.environ.get("BEDROCK_CROSS_ACCOUNT_ROLE_ARN") 46 | bedrock_region_parameter = "/AgenticLLMAssistantWorkshop/bedrock_region" 47 | 48 | BEDROCK_REGION = ssm.get_parameter(Name=bedrock_region_parameter) 49 | BEDROCK_REGION = BEDROCK_REGION["Parameter"]["Value"] 50 | 51 | retry_config = Config( 52 | region_name=BEDROCK_REGION, 53 | retries={"max_attempts": 10, "mode": "standard"} 54 | ) 55 | bedrock_runtime = boto3.client("bedrock-runtime", config=retry_config) 56 | bedrock = boto3.client("bedrock", config=retry_config) 57 | 58 | 59 | def activate_vector_extension(db_connection): 60 | """Activate PGVector extension.""" 61 | 62 | db_connection.autocommit = True 63 | cursor = db_connection.cursor() 64 | # install pgvector 65 | cursor.execute("CREATE EXTENSION IF NOT EXISTS vector;") 66 | db_connection.close() 67 | 68 | 69 | def test_db_connection(): 70 | # Connect to the database 71 | conn = psycopg2.connect( 72 | host=host, 73 | database=dbname, 74 | user=username, 75 | password=password 76 | ) 77 | # Get cursor 78 | cur = conn.cursor() 79 | 80 | # Query to get all tables 81 | cur.execute("SELECT table_name FROM information_schema.tables WHERE table_schema='public';") 82 | 83 | # Fetch all the tables 84 | tables = cur.fetchall() 85 | 86 | # Print the table names 87 | print(f"SQL tables: {tables}") 88 | 89 | # Close connection 90 | conn.close() 91 | 92 | 93 | def prepare_documents_with_metadata(documents_processed): 94 | 95 | langchain_documents_text = [] 96 | langchain_documents_tables = [] 97 | 98 | for document in documents_processed: 99 | document_name = document['name'] 100 | document_source_location = document['source_location'] 101 | document_s3_metadata = document['metadata'] 102 | 103 | mapping_to_original_page_numbers = { 104 | idx: pg_num for idx, pg_num 105 | in enumerate(document_s3_metadata["pages_kept"]) 106 | } 107 | # remove pages_kept since we already put the original page number. 108 | del document_s3_metadata["pages_kept"] 109 | 110 | for page in document['pages']: 111 | # Turn each page into a Langchain Document. 112 | # Note: you could choose to also prepend part of the previous document 113 | # and append part of the next document to include more context for documents 114 | # that have many pages which continue their text on the next page. 115 | current_metadata = { 116 | 'document_name': document_name, 117 | 'document_source_location': document_source_location, 118 | 'page_number': page['page'], 119 | 'original_page_number': mapping_to_original_page_numbers[page['page']] 120 | } 121 | # merge the document_s3_metadata into the langchain Document metadata 122 | # to be able to use them for filtering. 123 | current_metadata.update(document_s3_metadata) 124 | 125 | langchain_documents_text.append( 126 | Document( 127 | page_content=page['page_text'], 128 | metadata=current_metadata 129 | ) 130 | ) 131 | # Turn all the tables of the pages into seperate Langchain Documents as well 132 | # for table in page['page_tables']: 133 | # langchain_documents_tables.append( 134 | # Document( 135 | # page_content=table, 136 | # metadata=current_metadata 137 | # ) 138 | # ) 139 | 140 | # return langchain_documents_text, langchain_documents_tables 141 | return langchain_documents_text 142 | 143 | 144 | def load_processed_documents(json_file_path): 145 | with open(json_file_path, 'rb') as file: 146 | processed_documents = json.load(file) 147 | return processed_documents 148 | 149 | 150 | if __name__ == "__main__": 151 | test_db_connection() 152 | 153 | url_object = sqlalchemy.URL.create( 154 | "postgresql+psycopg2", 155 | username=username, 156 | password=password, 157 | host=host, 158 | database=dbname, 159 | ) 160 | 161 | input_data_base_path = "/opt/ml/processing/input/" 162 | processed_docs_filename = "documents_processed.json" 163 | token_split_chunk_size = 512 164 | token_chunk_overlap = 64 165 | # Define an embedding model to generate embeddings 166 | embedding_model_id = "amazon.titan-embed-text-v1" 167 | COLLECTION_NAME = 'agentic_assistant_vector_store' 168 | # make this an argument. 169 | pre_delete_collection = True 170 | 171 | db_engine = sqlalchemy.create_engine(url_object) 172 | 173 | processed_documents_file_path = os.path.join( 174 | input_data_base_path, 175 | "processed_documents", 176 | processed_docs_filename 177 | ) 178 | 179 | print(processed_documents_file_path) 180 | 181 | if os.path.isfile(processed_documents_file_path): 182 | processed_documents = load_processed_documents(processed_documents_file_path) 183 | langchain_documents_text = prepare_documents_with_metadata( 184 | processed_documents 185 | ) 186 | # The chunk overlap duplicates some text across chunks 187 | # to prevent context from being lost between chunks. 188 | # TODO: the following spliting uses tiktoken, 189 | # create a custom one that use the tokenizer from anthropic. 190 | text_splitter = TokenTextSplitter( 191 | chunk_size=token_split_chunk_size, 192 | chunk_overlap=token_chunk_overlap 193 | ) 194 | 195 | langchain_documents_text_chunked = text_splitter.split_documents( 196 | langchain_documents_text 197 | ) 198 | 199 | embedding_model = BedrockEmbeddings( 200 | model_id=embedding_model_id, 201 | client=bedrock_runtime 202 | ) 203 | 204 | activate_vector_extension(db_connection) 205 | 206 | pgvector_store = PGVector( 207 | collection_name=COLLECTION_NAME, 208 | connection_string=CONNECTION_STRING, 209 | embedding_function=embedding_model, 210 | pre_delete_collection=pre_delete_collection 211 | ) 212 | 213 | pgvector_store.add_documents(langchain_documents_text_chunked) 214 | 215 | print("test indexing results") 216 | test_question = "Who were in the board of directors of Amazon in 2021 and what were their positions?" 217 | print(pgvector_store.similarity_search_with_score(test_question)) 218 | 219 | else: 220 | raise ValueError(f"{processed_documents_file_path} must be a file.") 221 | 222 | test_db_connection() 223 | -------------------------------------------------------------------------------- /data_pipelines/scripts/prepare_documents.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pypdf import PdfReader, PdfWriter 3 | import shutil 4 | import os 5 | from textractor import Textractor 6 | from textractor.data.constants import TextractFeatures 7 | import boto3 8 | import logging 9 | from botocore.exceptions import ClientError 10 | import requests 11 | import sys 12 | 13 | sys.path.append(os.path.abspath(".")) 14 | from utils.helpers import store_list_to_s3 15 | 16 | import sagemaker 17 | 18 | default_sagemaker_bucket = sagemaker.Session().default_bucket() 19 | 20 | logger = logging.getLogger(__name__) 21 | logging.basicConfig(level=logging.INFO) 22 | 23 | def download_pdf_files(base_directory, docs_mapping, headers): 24 | # Create the base directory if it doesn't exist 25 | if not os.path.exists(base_directory): 26 | os.makedirs(base_directory) 27 | 28 | for company, docs in docs_mapping.items(): 29 | company_directory = os.path.join(base_directory, company) 30 | 31 | # Create a directory for the company if it doesn't exist 32 | if not os.path.exists(company_directory): 33 | os.makedirs(company_directory) 34 | 35 | for doc_info in docs: 36 | doc_url = doc_info["doc_url"] 37 | year = doc_info["year"] 38 | 39 | # Skip empty URLs 40 | if not doc_url: 41 | continue 42 | 43 | # Construct the filename based on the year and the URL 44 | filename = f"annual_report_{year}.pdf" 45 | file_path = os.path.join(company_directory, filename) 46 | 47 | # Check if the file already exists 48 | if os.path.exists(file_path): 49 | print(f"{filename} already exists for {company}") 50 | else: 51 | # Download the document 52 | response = requests.get(doc_url, headers=headers) 53 | 54 | if response.status_code == 200: 55 | with open(file_path, "wb") as file: 56 | file.write(response.content) 57 | print(f"Downloaded {filename} for {company}") 58 | else: 59 | print( 60 | f"Failed to download {filename} for {company}" 61 | f" (Status Code: {response.status_code})" 62 | ) 63 | 64 | def keep_relevant_pages_in_pdf(input_pdf_path, output_pdf_path, pages): 65 | input_pdf = PdfReader(input_pdf_path) 66 | print(f"Number of pages is {len(input_pdf.pages)}") 67 | print(f"Relevant pages are {pages}") 68 | output_pdf = PdfWriter() 69 | 70 | for page_num in pages: 71 | output_pdf.add_page(input_pdf.pages[page_num - 1]) 72 | 73 | with open(output_pdf_path, "wb") as f: 74 | output_pdf.write(f) 75 | 76 | 77 | def save_json(json_data, file_path): 78 | with open(file_path, "w") as f: 79 | json.dump(json_data, f) 80 | 81 | def keep_relevant_pages_in_pdfs( 82 | raw_base_directory, prepared_base_directory, docs_mapping 83 | ): 84 | metadata = [] 85 | # Create the base directory if it doesn't exist 86 | if not os.path.exists(prepared_base_directory): 87 | os.makedirs(prepared_base_directory) 88 | 89 | for company, docs in docs_mapping.items(): 90 | raw_company_directory = os.path.join(raw_base_directory, company) 91 | prepared_company_directory = os.path.join(prepared_base_directory, company) 92 | 93 | # Create a directory for the company if it doesn't exist 94 | if not os.path.exists(prepared_company_directory): 95 | os.makedirs(prepared_company_directory) 96 | 97 | for doc_info in docs: 98 | doc_url = doc_info["doc_url"] 99 | year = doc_info["year"] 100 | pages = doc_info.get("pages", []) 101 | if not doc_url: 102 | continue 103 | 104 | current_metadata = {} 105 | current_metadata["company"] = company 106 | current_metadata["year"] = year 107 | current_metadata["doc_url"] = doc_url 108 | 109 | # Construct the filename based on the year and the URL 110 | filename = f"annual_report_{year}.pdf" 111 | input_pdf_path = os.path.join(raw_company_directory, filename) 112 | output_pdf_path = os.path.join(prepared_company_directory, filename) 113 | 114 | current_metadata["local_pdf_path"] = output_pdf_path 115 | 116 | if not pages: 117 | # When page numbers are not defined, we assume the user wants 118 | # to process the full file, therefore, copy it as is 119 | # to the prepared folder 120 | shutil.copyfile(input_pdf_path, output_pdf_path) 121 | metadata.append(current_metadata) 122 | continue 123 | 124 | relevant_pages = doc_info["pages"] 125 | current_metadata["pages_kept"] = relevant_pages 126 | 127 | # Skip empty URLs 128 | 129 | keep_relevant_pages_in_pdf(input_pdf_path, output_pdf_path, relevant_pages) 130 | 131 | metadata.append(current_metadata) 132 | 133 | save_json(metadata, os.path.join(prepared_base_directory, "metadata.json")) 134 | 135 | return True 136 | 137 | def extract_documents(): 138 | region = boto3.session.Session().region_name 139 | # extractor = Textractor(profile_name="default") 140 | extractor = Textractor(region_name=region) 141 | 142 | input_document = "raw_documents/prepared/Amazon/annual_report_2022.pdf" 143 | 144 | document = extractor.start_document_analysis( 145 | file_source=input_document, 146 | s3_upload_path=f"s3://{default_sagemaker_bucket}/input_documents/", 147 | s3_output_path=f"s3://{default_sagemaker_bucket}/output_documents/", 148 | features=[TextractFeatures.LAYOUT], 149 | save_image=False 150 | ) 151 | 152 | def generate_message(bedrock_runtime, model_id, system_prompt, messages, max_tokens): 153 | 154 | body=json.dumps( 155 | { 156 | "anthropic_version": "bedrock-2023-05-31", 157 | "max_tokens": max_tokens, 158 | "system": system_prompt, 159 | "messages": messages 160 | } 161 | ) 162 | response = bedrock_runtime.invoke_model(body=body, modelId=model_id) 163 | response_body = json.loads(response.get('body').read()) 164 | 165 | return response_body 166 | 167 | 168 | def call_llm(user_input, model_id, system_prompt, bedrock_runtime, max_tokens=1000): 169 | """Handle calls to Anthropic Claude message api.""" 170 | try: 171 | # Prompt with user turn only. 172 | user_message = {"role": "user", "content": user_input} 173 | messages = [user_message] 174 | return generate_message(bedrock_runtime, model_id, system_prompt, messages, max_tokens) 175 | except ClientError as err: 176 | message=err.response["Error"]["Message"] 177 | logger.error("A client error occurred: %s", message) 178 | print("A client error occured: " + 179 | format(message)) 180 | 181 | user_prompt = """ 182 | Improve the markdown while keeping all original information. Put the improved markdown inside a xml tags with no explanation: 183 | \n{markdown_doc} 184 | """.strip() 185 | 186 | system_prompt = "Your task is to review and improve the results of Amazon textract in markdown." 187 | 188 | 189 | def improve_textract_markdown_output(document, llm_model_id): 190 | improved_markdown = [] 191 | for i in range(len(document.pages)): 192 | user_input = user_prompt.format(markdown_doc=document.pages[i].to_markdown()) 193 | result = call_llm(user_input, llm_model_id, system_prompt, bedrock_runtime, max_tokens=3000) 194 | # Extract the text between the XML tags only. 195 | improved_markdown.append(result["content"][0]["text"].split("")[-1].split("")[0].strip()) 196 | return improved_markdown 197 | 198 | def extract_pages_as_markdown(input_document): 199 | extractor = Textractor() 200 | document = extractor.start_document_analysis( 201 | file_source=input_document, 202 | s3_upload_path=f"s3://{default_sagemaker_bucket}/input_documents/", 203 | s3_output_path=f"s3://{default_sagemaker_bucket}/output_documents/", 204 | features=[TextractFeatures.LAYOUT], 205 | save_image=False 206 | ) 207 | 208 | res = improve_textract_markdown_output(document, llm_model_id) 209 | pages = [{"page": indx, "page_text": text} for indx, text in enumerate(res)] 210 | return pages 211 | 212 | 213 | def extract_docs_into_markdown(docs_metadata): 214 | results = [] 215 | for doc_meta in docs_metadata: 216 | doc_result_with_metadata = {} 217 | doc_result_with_metadata["metadata"] = doc_meta 218 | doc_result_with_metadata["name"] = doc_meta["doc_url"].split("/")[-1] 219 | doc_result_with_metadata["source_location"] = doc_meta["doc_url"] 220 | doc_result_with_metadata["pages"] = extract_pages_as_markdown(doc_meta["local_pdf_path"]) 221 | results.append(doc_result_with_metadata) 222 | return results 223 | 224 | 225 | docs_mapping = { 226 | "Amazon": [ 227 | { 228 | "doc_url": "https://s2.q4cdn.com/299287126/files/doc_financials/2023/ar/Amazon-2022-Annual-Report.pdf", 229 | "year": "2022", 230 | "pages": [15, 17, 18, 47, 48], 231 | }, 232 | { 233 | "doc_url": "https://s2.q4cdn.com/299287126/files/doc_financials/2022/ar/Amazon-2021-Annual-Report.pdf", 234 | "year": "2021", 235 | "pages": [14, 16, 17, 18, 46, 47], 236 | }, 237 | {"doc_url": "", "year": ""}, 238 | ] 239 | } 240 | 241 | bedrock_runtime = boto3.client("bedrock-runtime", region_name="us-west-2") 242 | llm_model_id = "anthropic.claude-3-sonnet-20240229-v1:0" 243 | 244 | 245 | if __name__ == "__main__": 246 | raw_base_directory = "raw_documents" 247 | if not os.path.exists(raw_base_directory): 248 | os.makedirs(raw_base_directory) 249 | 250 | prepared_base_directory = os.path.join(raw_base_directory, "prepared/") 251 | headers = { 252 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" 253 | } 254 | download_pdf_files(raw_base_directory, docs_mapping, headers) 255 | 256 | keep_relevant_pages_in_pdfs(raw_base_directory, prepared_base_directory, docs_mapping) 257 | with open( 258 | os.path.join(prepared_base_directory, "metadata.json"), "r" 259 | ) as prepared_pdfs_metadata_obj: 260 | prepared_pdfs_metadata = json.load(prepared_pdfs_metadata_obj) 261 | results = extract_docs_into_markdown(prepared_pdfs_metadata) 262 | ssm = boto3.client("ssm") 263 | s3_bucket_name_parameter = "/AgenticLLMAssistantWorkshop/AgentDataBucketParameter" 264 | s3_bucket_name = ssm.get_parameter(Name=s3_bucket_name_parameter) 265 | s3_bucket_name = s3_bucket_name["Parameter"]["Value"] 266 | processed_documents_s3_key = "documents_processed.json" 267 | store_list_to_s3(s3_bucket_name, processed_documents_s3_key, results) 268 | print("Done!") -------------------------------------------------------------------------------- /data_pipelines/scripts/requirements.txt: -------------------------------------------------------------------------------- 1 | # The specified boto versions recognize Amazon Bedrock service after general availability. 2 | boto3>=1.28.57 3 | botocore>=1.31.57 4 | pandas==2.0.3 5 | dask[dataframe]==2023.10.0 6 | tabulate 7 | langchain==0.2.12 8 | langchain-aws==0.1.16 9 | langchain-community==0.2.11 10 | psycopg2-binary==2.9.9 11 | # tiktoken is required by langchain.text_splitter.TokenTextSplitter 12 | tiktoken 13 | pgvector==0.2.5 14 | sqlalchemy==2.0.30 -------------------------------------------------------------------------------- /data_pipelines/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/build-an-agentic-llm-assistant/1e4b2bea103b0af9b4f9aeffa620231dc1867002/data_pipelines/utils/__init__.py -------------------------------------------------------------------------------- /data_pipelines/utils/helpers.py: -------------------------------------------------------------------------------- 1 | import json 2 | import boto3 3 | 4 | # Initialize the S3 client 5 | s3_client = boto3.client("s3") 6 | 7 | 8 | def store_list_to_s3(bucket_name, object_key, data): 9 | # Serialize the list using json 10 | json_data = json.dumps(data) 11 | # Upload the json data to S3 12 | s3_client.put_object(Bucket=bucket_name, Key=object_key, Body=json_data) 13 | 14 | 15 | def load_list_from_s3(bucket_name, object_key): 16 | # Download the json data from S3 17 | response = s3_client.get_object(Bucket=bucket_name, Key=object_key) 18 | json_data = response["Body"].read() 19 | 20 | data = json.loads(json_data) 21 | return data -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | !jest.config.js 2 | *.d.ts 3 | node_modules 4 | 5 | # CDK asset staging directory 6 | .cdk.staging 7 | cdk.out 8 | cdk.context.json 9 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to this frontend deployment CDK 2 | 3 | This CDK project builds the infrastructure required to setup and deploy the Next.js app inside `chat-app` using AWS Amplify. 4 | Upon deployment you will get a standalone full-featured chat user experience with Amazon Cognito authentication. 5 | 6 | The `cdk.json` file tells the CDK Toolkit how to execute your app. 7 | 8 | ## What gets built 9 | 10 | - [x] An AWS CodeCommit repository with a copy of the Next.js app code in `chat-app`. 11 | - [x] An AWS Amplify CI/CD to build, deploy, and host the Next.js. 12 | - [x] Setup with Amazon Cognito user pool from the backend CDK to enable user authentication. 13 | - [x] Connctivity with REST API build with the backend CDK. 14 | 15 | ## Useful commands 16 | 17 | * `npm run build` compile typescript to js 18 | * `npm run watch` watch for changes and compile 19 | * `npm run test` perform the jest unit tests 20 | * `npx cdk deploy` deploy this stack to your default AWS account/region 21 | * `npx cdk diff` compare deployed stack with current state 22 | * `npx cdk synth` emits the synthesized CloudFormation template 23 | 24 | If you are looking for a simple experimentation UI, you can use streamlit as explained in `experiments/streamlit-ui/README.md`. 25 | -------------------------------------------------------------------------------- /frontend/bin/amplify-chatui.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from 'aws-cdk-lib'; 4 | import { AmplifyChatuiStack } from '../lib/amplify-chatui-stack'; 5 | 6 | const app = new cdk.App(); 7 | new AmplifyChatuiStack(app, 'AmplifyChatuiStack', { 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 | 12 | /* Uncomment the next line to specialize this stack for the AWS Account 13 | * and Region that are implied by the current CLI configuration. */ 14 | // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, 15 | 16 | /* Uncomment the next line if you know exactly what Account and Region you 17 | * want to deploy the stack to. */ 18 | // env: { account: '123456789012', region: 'us-east-1' }, 19 | 20 | /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ 21 | }); 22 | 23 | cdk.Tags.of(app).add("project", "chatui"); 24 | -------------------------------------------------------------------------------- /frontend/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/amplify-chatui.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 33 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 34 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 35 | "@aws-cdk/core:enablePartitionLiterals": true, 36 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 37 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 38 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 39 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 40 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 41 | "@aws-cdk/aws-route53-patters:useCertificate": true, 42 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 43 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 44 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 45 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 46 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 47 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 48 | "@aws-cdk/aws-redshift:columnId": true, 49 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 50 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 51 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 52 | "@aws-cdk/aws-kms:aliasNameRef": true, 53 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 54 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 55 | "@aws-cdk/aws-efs:denyAnonymousAccess": true, 56 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, 57 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, 58 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, 59 | "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, 60 | "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, 61 | "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, 62 | "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /frontend/chat-app/.env: -------------------------------------------------------------------------------- 1 | # here we prefix the environment variables needed in the client/browser side 2 | # with NEXT_PUBLIC_ so they are made publicly accessible by Next.js 3 | # see https://nextjs.org/docs/pages/building-your-application/configuring/environment-variables#bundling-environment-variables-for-the-browser 4 | # without adding NEXT_PUBLIC_ to cognito ids we get the error "Auth UserPool not configured." 5 | 6 | NEXT_PUBLIC_AMPLIFY_USERPOOL_ID=$AMPLIFY_USERPOOL_ID 7 | NEXT_PUBLIC_COGNITO_USERPOOL_CLIENT_ID=$COGNITO_USERPOOL_CLIENT_ID 8 | NEXT_PUBLIC_API_ENDPOINT=$API_ENDPOINT -------------------------------------------------------------------------------- /frontend/chat-app/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /frontend/chat-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /frontend/chat-app/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /frontend/chat-app/amplify.yml: -------------------------------------------------------------------------------- 1 | 2 | # https://docs.aws.amazon.com/amplify/latest/userguide/deploy-nextjs-app.html 3 | 4 | version: 1 5 | frontend: 6 | phases: 7 | preBuild: 8 | commands: 9 | - nvm use 18 10 | - npm ci 11 | build: 12 | commands: 13 | - npm run build 14 | artifacts: 15 | baseDirectory: .next 16 | files: 17 | - '**/*' 18 | cache: 19 | paths: 20 | - node_modules/**/* 21 | -------------------------------------------------------------------------------- /frontend/chat-app/app/aws-exports.tsx: -------------------------------------------------------------------------------- 1 | // https://docs.amplify.aws/nextjs/build-a-backend/auth/set-up-auth/ 2 | import { Amplify } from 'aws-amplify'; 3 | 4 | Amplify.configure({ 5 | Auth: { 6 | Cognito: { 7 | // Amazon Cognito User Pool ID 8 | userPoolId: process.env.NEXT_PUBLIC_AMPLIFY_USERPOOL_ID || '', 9 | // OPTIONAL - Amazon Cognito Web Client ID (26-char alphanumeric string) 10 | userPoolClientId: process.env.NEXT_PUBLIC_COGNITO_USERPOOL_CLIENT_ID || '', 11 | // REQUIRED only for Federated Authentication - Amazon Cognito Identity Pool ID 12 | // identityPoolId: 'XX-XXXX-X:XXXXXXXX-XXXX-1234-abcd-1234567890ab', 13 | // OPTIONAL - This is used when autoSignIn is enabled for Auth.signUp 14 | // 'code' is used for Auth.confirmSignUp, 'link' is used for email link verification 15 | signUpVerificationMethod: 'code', // 'code' | 'link' 16 | } 17 | } 18 | }); 19 | 20 | // You can get the current config object 21 | const currentConfig = Amplify.getConfig(); 22 | 23 | export default currentConfig; -------------------------------------------------------------------------------- /frontend/chat-app/app/components/ChatApp.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useState, useEffect, useRef } from "react"; 4 | import ChatMessage from "./ChatMessage"; 5 | import ClearButton from "./ClearButton"; 6 | import SelectMode from "./SelectMode"; 7 | import DebugToggleSwitch from "./DebugToggleSwitch"; 8 | 9 | import { IconSend2 } from "@tabler/icons-react"; 10 | import { fetchAuthSession, fetchUserAttributes } from "aws-amplify/auth"; 11 | 12 | interface Message { 13 | content: string; 14 | isUser: boolean; 15 | } 16 | 17 | const ChatApp: React.FC = () => { 18 | const [messages, setMessages] = useState([]); 19 | const [clean_history, setCleanHistory] = useState(false); 20 | const [debugMode, setDebugMode] = useState(false); 21 | const [assistantMode, setAssistantMode] = useState<"basic" | "agentic">( 22 | "basic" 23 | ); 24 | const messageContainerRef = useRef(null); 25 | 26 | useEffect(() => { 27 | const storedDebugMode = localStorage.getItem("debugMode"); 28 | setDebugMode(storedDebugMode === "true"); 29 | 30 | const storedAssistantMode = localStorage.getItem("assistantMode"); 31 | setAssistantMode(storedAssistantMode === "agentic" ? "agentic" : "basic"); 32 | }, []); 33 | 34 | const handleSendMessage = async (message: string) => { 35 | const defaultErrorMessage = 36 | "Error while preparing your answer. Check your connectivity to the backend."; 37 | // fetch the cognito authentication tokens to use with the API call. 38 | const authToken = (await fetchAuthSession()).tokens?.idToken?.toString(); 39 | const userAttributes = await fetchUserAttributes(); 40 | // Append the user's input message to the message container immediately 41 | setMessages((prevMessages) => [ 42 | ...prevMessages, 43 | { content: message, isUser: true }, 44 | ]); 45 | 46 | // Call the API to get the response 47 | const rest_api_endpoint = process.env.NEXT_PUBLIC_API_ENDPOINT ?? ""; 48 | try { 49 | const response = await fetch(rest_api_endpoint, { 50 | // mode: "no-cors", 51 | method: "POST", 52 | headers: { 53 | "Content-Type": "application/json", 54 | Authorization: authToken || "", 55 | }, 56 | // Currently we use the cognito user.sub as a session_id 57 | // this needs to be updated if one wants to store multiple chats for the same user. 58 | body: JSON.stringify({ 59 | user_input: message, 60 | session_id: userAttributes.sub, 61 | clean_history: clean_history, 62 | chatbot_type: assistantMode, 63 | }), 64 | }); 65 | const responseData = await response.json(); 66 | // Add the response to the messages state after receiving it 67 | setCleanHistory(false); 68 | let AIMessage: Message; 69 | if (responseData.errorMessage && debugMode) { 70 | AIMessage = { 71 | content: `Error: ${ 72 | responseData.errorMessage 73 | }\n\nDetails: \n\n\`\`\`\n\n${JSON.stringify( 74 | responseData, 75 | null, 76 | 2 77 | )}\n\n\`\`\``, 78 | isUser: false, 79 | }; 80 | } else if (responseData.errorMessage) { 81 | AIMessage = { content: defaultErrorMessage, isUser: false }; 82 | } else { 83 | AIMessage = { content: responseData.response, isUser: false }; 84 | } 85 | setMessages((prevMessages) => [...prevMessages, AIMessage]); 86 | } catch { 87 | setMessages((prevMessages) => [ 88 | ...prevMessages, 89 | { content: defaultErrorMessage, isUser: false }, 90 | ]); 91 | } 92 | }; 93 | 94 | // below useEffect handles automatic scroll down when the messages content 95 | // overflows the container. 96 | useEffect(() => { 97 | if (messageContainerRef.current) { 98 | messageContainerRef.current.scrollTop = 99 | messageContainerRef.current.scrollHeight; 100 | } 101 | }, [messages]); 102 | 103 | return ( 104 |
105 |
106 |
110 | {messages.map((message, index) => ( 111 | 116 | ))} 117 |
118 |
119 | { 125 | // send message on enter if not empty 126 | if (e.key === "Enter") { 127 | if (e.currentTarget.value !== "") { 128 | handleSendMessage(e.currentTarget.value); 129 | e.currentTarget.value = ""; 130 | } 131 | } 132 | }} 133 | autoComplete="off" 134 | /> 135 | 152 |
153 |
154 | setDebugMode(value)} /> 155 | { 157 | setAssistantMode(mode); 158 | }} 159 | /> 160 | { 162 | // Handle clearing the conversation 163 | setMessages([]); 164 | setCleanHistory(true); 165 | }} 166 | /> 167 |
168 |
169 |
170 | ); 171 | }; 172 | 173 | export default ChatApp; 174 | -------------------------------------------------------------------------------- /frontend/chat-app/app/components/ChatMessage.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Markdown from "react-markdown"; 3 | 4 | interface ChatMessageProps { 5 | content: string; 6 | isUser: boolean; 7 | } 8 | 9 | const ChatMessage: React.FC = ({ content, isUser }) => { 10 | const initials = isUser ? "You" : "Ai"; 11 | // Use green for user, blue for AI 12 | const bgColor = isUser ? "bg-gray-500" : "bg-purple-500"; 13 | 14 | return ( 15 |
16 | {isUser ? ( 17 |
18 |
21 | {initials} 22 |
23 | 24 |
25 |
26 | {content} 27 |
28 |
29 |
30 |
31 | ) : ( 32 |
33 |
34 |
35 |
36 | {content} 37 |
38 |
39 |
42 | {initials} 43 |
44 |
45 | )} 46 |
47 | ); 48 | }; 49 | 50 | export default ChatMessage; 51 | -------------------------------------------------------------------------------- /frontend/chat-app/app/components/ClearButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { IconTrash } from "@tabler/icons-react"; 3 | 4 | interface ClearButtonProps { 5 | onClick: () => void; 6 | } 7 | 8 | const ClearButton: React.FC = ({ onClick }) => { 9 | return ( 10 | 17 | ); 18 | }; 19 | 20 | export default ClearButton; 21 | -------------------------------------------------------------------------------- /frontend/chat-app/app/components/DebugToggleSwitch.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | 3 | interface DebugToggleSwitchProps { 4 | onToggle: (value: boolean) => void; 5 | } 6 | 7 | const DebugToggleSwitch: React.FC = ({ onToggle }) => { 8 | const [enabled, setEnabled] = useState(false); 9 | 10 | useEffect(() => { 11 | const debugMode = localStorage.getItem('debugMode'); 12 | setEnabled(debugMode === 'true'); 13 | }, []); 14 | 15 | const toggleSwitch = (event: React.ChangeEvent) => { 16 | const value = event.target.checked; 17 | localStorage.setItem('debugMode', String(value)); 18 | setEnabled(value); 19 | onToggle(value); 20 | }; 21 | 22 | return ( 23 | 32 | ); 33 | }; 34 | 35 | export default DebugToggleSwitch; -------------------------------------------------------------------------------- /frontend/chat-app/app/components/SelectMode.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | interface SelectModeProps { 4 | onClick: (mode: 'basic' | 'agentic') => void; 5 | } 6 | 7 | const SelectMode: React.FC = ({ onClick }) => { 8 | // fetch the assistantMode from cache if available otherwise default to basic 9 | const [mode, setMode] = useState<'basic' | 'agentic'>(() => { 10 | const storedMode = localStorage.getItem('assistantMode'); 11 | return storedMode === 'agentic' ? 'agentic' : 'basic'; 12 | }); 13 | 14 | const handleModeChange = (e: React.ChangeEvent) => { 15 | const selectedMode = e.target.value as 'basic' | 'agentic'; 16 | localStorage.setItem('assistantMode', String(selectedMode)); 17 | setMode(selectedMode); 18 | onClick(selectedMode); 19 | }; 20 | 21 | return ( 22 |
23 | Assistant Mode 24 | 33 | 42 |
43 | ); 44 | }; 45 | 46 | export default SelectMode; -------------------------------------------------------------------------------- /frontend/chat-app/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/build-an-agentic-llm-assistant/1e4b2bea103b0af9b4f9aeffa620231dc1867002/frontend/chat-app/app/favicon.ico -------------------------------------------------------------------------------- /frontend/chat-app/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /*:root {*/ 6 | /* --foreground-rgb: 0, 0, 0;*/ 7 | /* --background-start-rgb: 214, 219, 220;*/ 8 | /* --background-end-rgb: 255, 255, 255;*/ 9 | /*}*/ 10 | 11 | /*@media (prefers-color-scheme: dark) {*/ 12 | /* :root {*/ 13 | /* --foreground-rgb: 255, 255, 255;*/ 14 | /* --background-start-rgb: 0, 0, 0;*/ 15 | /* --background-end-rgb: 0, 0, 0;*/ 16 | /* }*/ 17 | /*}*/ 18 | 19 | /*body {*/ 20 | /* color: rgb(var(--foreground-rgb));*/ 21 | /* background: linear-gradient(*/ 22 | /* to bottom,*/ 23 | /* transparent,*/ 24 | /* rgb(var(--background-end-rgb))*/ 25 | /* )*/ 26 | /* rgb(var(--background-start-rgb));*/ 27 | /*}*/ 28 | -------------------------------------------------------------------------------- /frontend/chat-app/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | import { Inter } from 'next/font/google' 3 | import './globals.css' 4 | 5 | const inter = Inter({ subsets: ['latin'] }) 6 | 7 | export const metadata: Metadata = { 8 | title: 'Agentic Documents Assistant', 9 | description: 'Chatty, the LLM Agentic Documents Assistant.', 10 | } 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: { 15 | children: React.ReactNode 16 | }) { 17 | return ( 18 | 19 | {children} 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /frontend/chat-app/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Amplify } from "aws-amplify"; 4 | import { Authenticator } from "@aws-amplify/ui-react"; 5 | import "@aws-amplify/ui-react/styles.css"; 6 | import ChatApp from "./components/ChatApp"; 7 | import awsconfig from "./aws-exports"; 8 | 9 | Amplify.configure(awsconfig, { ssr: true }); 10 | 11 | export default function IndexPage() { 12 | const formFields = { 13 | signUp: { 14 | username: { 15 | order: 1, 16 | }, 17 | email: { 18 | order: 2, 19 | }, 20 | password: { 21 | order: 3, 22 | }, 23 | confirm_password: { 24 | order: 4, 25 | }, 26 | }, 27 | }; 28 | 29 | return ( 30 | 31 | {({ signOut, user }) => ( 32 |
33 |
34 |

35 | Chatty 36 |

37 |
38 |
39 | Welcome {user?.username}! 40 |
41 | 47 |
48 |
49 |
50 | 51 |
52 |
53 |

54 | © {new Date().getFullYear()} Your Company, all rights 55 | reserved. 56 |

57 |

58 | AI answers should be verified before use. 59 |

60 |
61 |
62 | )} 63 |
64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /frontend/chat-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatui", 3 | "version": "0.1.0", 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": "^6.0.7", 13 | "@tabler/icons-react": "^3.21.0", 14 | "@tailwindcss/typography": "^0.5.13", 15 | "aws-amplify": "^6.0.12", 16 | "next": "^14.2.10", 17 | "react": "^18", 18 | "react-dom": "^18", 19 | "react-markdown": "^9.0.1" 20 | }, 21 | "devDependencies": { 22 | "@types/node": "^20", 23 | "@types/react": "^18", 24 | "@types/react-dom": "^18", 25 | "autoprefixer": "^10.4.16", 26 | "eslint": "^8", 27 | "eslint-config-next": "14.0.4", 28 | "postcss": "^8.4.33", 29 | "tailwindcss": "^3.4.1", 30 | "typescript": "^5" 31 | } 32 | } -------------------------------------------------------------------------------- /frontend/chat-app/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /frontend/chat-app/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | const config: Config = { 4 | content: [ 5 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './app/**/*.{js,ts,jsx,tsx,mdx}', 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 13 | 'gradient-conic': 14 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 15 | }, 16 | }, 17 | }, 18 | plugins: [require('@tailwindcss/typography')], 19 | } 20 | export default config 21 | -------------------------------------------------------------------------------- /frontend/chat-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /frontend/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 | -------------------------------------------------------------------------------- /frontend/lib/amplify-chatui-stack.ts: -------------------------------------------------------------------------------- 1 | import * as amplify from '@aws-cdk/aws-amplify-alpha'; 2 | import * as cdk from 'aws-cdk-lib'; 3 | import { Construct } from 'constructs'; 4 | import * as codecommit from 'aws-cdk-lib/aws-codecommit'; 5 | import * as ssm from 'aws-cdk-lib/aws-ssm'; 6 | import * as lambda from 'aws-cdk-lib/aws-lambda'; 7 | import * as apigateway from "aws-cdk-lib/aws-apigateway"; 8 | import * as cognito from 'aws-cdk-lib/aws-cognito'; 9 | import * as iam from 'aws-cdk-lib/aws-iam'; 10 | import path = require('path'); 11 | 12 | export class AmplifyChatuiStack extends cdk.Stack { 13 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 14 | super(scope, id, props); 15 | 16 | // ------------------------------------------------------------------------- 17 | // Load SSM parameter that stores the Lambda function name 18 | 19 | const cognito_user_pool_id_parameter = ssm.StringParameter.valueForStringParameter( 20 | this, "/AgenticLLMAssistantWorkshop/cognito_user_pool_id" 21 | ); 22 | 23 | const cognito_user_pool_client_id_parameter = ssm.StringParameter.valueForStringParameter( 24 | this, "/AgenticLLMAssistantWorkshop/cognito_user_pool_client_id" 25 | ); 26 | 27 | // SSM parameter holding Rest API URL 28 | const agent_api_parameter = ssm.StringParameter.valueForStringParameter( 29 | this, "/AgenticLLMAssistantWorkshop/agent_api" 30 | ); 31 | 32 | // ------------------------------------------------------------------------- 33 | 34 | // create a new repository and initialize it with the chatui nextjs app source code. 35 | // https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_codecommit-readme.html 36 | const amplifyChatUICodeCommitRepo = new codecommit.Repository(this, 'NextJsGitRepository', { 37 | repositoryName: 'nextjs-amplify-chatui', 38 | description: 'A chatui with nextjs hosted on AWS Amplify.', 39 | code: codecommit.Code.fromDirectory(path.join(__dirname, '../chat-app'), 'main') 40 | }); 41 | 42 | // from https://docs.aws.amazon.com/cdk/api/v2/docs/aws-amplify-alpha-readme.html 43 | const amplifyChatUI = new amplify.App(this, 'AmplifyNextJsChatUI', { 44 | autoBranchDeletion: true, 45 | sourceCodeProvider: new amplify.CodeCommitSourceCodeProvider( 46 | {repository: amplifyChatUICodeCommitRepo}), 47 | // enable server side rendering 48 | platform: amplify.Platform.WEB_COMPUTE, 49 | // https://docs.aws.amazon.com/amplify/latest/userguide/environment-variables.html#amplify-console-environment-variables 50 | environmentVariables: { 51 | // the following custom image is used to support Next.js 14, see links for details: 52 | // 1. https://aws.amazon.com/blogs/mobile/6-new-aws-amplify-launches-to-make-frontend-development-easier/ 53 | // 2. https://github.com/aws-cloudformation/cloudformation-coverage-roadmap/issues/1299 54 | '_CUSTOM_IMAGE': 'amplify:al2023', 55 | 'AMPLIFY_USERPOOL_ID': cognito_user_pool_id_parameter, 56 | 'COGNITO_USERPOOL_CLIENT_ID': cognito_user_pool_client_id_parameter, 57 | 'API_ENDPOINT': agent_api_parameter 58 | } 59 | }); 60 | 61 | amplifyChatUI.addBranch('main', {stage: "PRODUCTION"}); 62 | 63 | // ----------------------------------------------------------------------- 64 | // stack outputs 65 | 66 | new cdk.CfnOutput(this, "AmplifyAppURL", { 67 | value: amplifyChatUI.defaultDomain, 68 | }); 69 | 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "amplify-chatui", 3 | "version": "0.1.0", 4 | "bin": { 5 | "amplify-chatui": "bin/amplify-chatui.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@tailwindcss/typography": "^0.5.13", 15 | "@types/jest": "^29.5.11", 16 | "@types/node": "20.10.4", 17 | "aws-cdk": "2.115.0", 18 | "jest": "^29.7.0", 19 | "ts-jest": "^29.1.1", 20 | "ts-node": "^10.9.2", 21 | "typescript": "~5.3.3" 22 | }, 23 | "dependencies": { 24 | "@aws-cdk/aws-amplify-alpha": "^2.115.0-alpha.0", 25 | "aws-cdk-lib": "2.115.0", 26 | "constructs": "^10.0.0", 27 | "source-map-support": "^0.5.21" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /frontend/test/amplify-chatui.test.ts: -------------------------------------------------------------------------------- 1 | // import * as cdk from 'aws-cdk-lib'; 2 | // import { Template } from 'aws-cdk-lib/assertions'; 3 | // import * as AmplifyChatui from '../lib/amplify-chatui-stack'; 4 | 5 | // example test. To run these tests, uncomment this file along with the 6 | // example resource in lib/amplify-chatui-stack.ts 7 | test('SQS Queue Created', () => { 8 | // const app = new cdk.App(); 9 | // // WHEN 10 | // const stack = new AmplifyChatui.AmplifyChatuiStack(app, 'MyTestStack'); 11 | // // THEN 12 | // const template = Template.fromStack(stack); 13 | 14 | // template.hasResourceProperties('AWS::SQS::Queue', { 15 | // VisibilityTimeout: 300 16 | // }); 17 | }); 18 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2020", 7 | "dom" 8 | ], 9 | "declaration": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": false, 19 | "inlineSourceMap": true, 20 | "inlineSources": true, 21 | "experimentalDecorators": true, 22 | "strictPropertyInitialization": false, 23 | "typeRoots": [ 24 | "./node_modules/@types" 25 | ] 26 | }, 27 | "exclude": [ 28 | "node_modules", 29 | "cdk.out" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /lab_01_llm_apps_development_tools/README.md: -------------------------------------------------------------------------------- 1 | ## Goal 2 | 3 | Understand the value of the tools used to build applications on the cloud, 4 | namely the idea of infrastructure as a code (IaC) using AWS CloudFormation 5 | and AWS CDK. 6 | 7 | ## Concepts 8 | 9 | When developing applications powered by Large Language Models (LLMs), 10 | it's crucial to understand that the process is fundamentally an application development exercise, 11 | much like traditional software development. 12 | While Jupyter Notebooks provide a convenient environment for prototyping and experimentation, 13 | they have limitations when it comes to production deployment and scalability. 14 | This is where traditional application tools such as Infrastructure as Code (IaC) becomes indispensable. 15 | 16 | IaC enables developers to treat infrastructure provisioning and configuration as code, allowing for consistent, repeatable, 17 | and automated deployment of resources across different environments. 18 | By embracing IaC, LLM application developers can leverage the same best practices and methodologies that have proven successful in traditional application development, 19 | such as version control, continuous integration/continuous deployment (CI/CD), and infrastructure testing. 20 | 21 | Translating LLM applications from Notebooks to IaC-based deployments on AWS Cloud not only ensures scalability and reliability but also promotes collaboration, maintainability, and governance. 22 | Traditional software development practices, including modular design, code reviews, and testing, remain equally relevant in the context of LLM application development, ensuring that the applications are robust, secure, and capable of handling real-world workloads. 23 | 24 | By adopting IaC and embracing traditional application development practices, 25 | LLM application developers can streamline the transition from experimentation to production, 26 | enabling them to build and deploy LLM-powered applications at scale while adhering to industry-standard best practices. 27 | 28 | ## Lab 29 | 30 | * Task 1: Create an S3 bucket manully through the AWS console. 31 | * Follow the instructor instruction, or the documentation at [Step 1: Create your first S3 bucket](https://docs.aws.amazon.com/AmazonS3/latest/userguide/creating-bucket.html) 32 | * Task 2: Create an S3 bucket using AWS CloudFormation through the console 33 | * Use the cloudformation in `lab1.1_cloudformation_s3.yaml`. 34 | * Follow the instructor's demo, or the documentation on [Creating a stack on the AWS CloudFormation console](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-console-create-stack.html) 35 | * Make sure to delete the stack through the console after you finish this exercise. 36 | * Task 3: Create an S3 bucket using AWS CDK 37 | * Use the CDK stack within the folder `lab1.2_s3_cdk` and follow the insturctions below: 38 | 1. Go into the stack's folder `cd lab1.2_s3_cdk`. 39 | 2. Run `npm clean-install` to install the stack's dependencies. 40 | 3. Run `npx cdk bootstrap` to setup CDK on your account. This only needs to happen once per AWS account and region combination. 41 | 4. Then install the stack using `npx cdk deploy` 42 | * One the Stack deployment is finished, check the newly created bucket on Amazon S3. 43 | * Make sure to destroy the stack after the exercise is finished using `npx cdk destroy` 44 | * Task 4: call to action - reflect on the benefit of using Infrastructure as Code. 45 | -------------------------------------------------------------------------------- /lab_01_llm_apps_development_tools/lab1.1_cloudformation_s3.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Description: 'Creates an S3 bucket with a provided name' 3 | 4 | Resources: 5 | MyS3Bucket: 6 | Type: AWS::S3::Bucket 7 | Properties: 8 | BucketName: !Sub 'genai-bootcamp-cfn-bucket-${AWS::AccountId}-${AWS::Region}' 9 | BucketEncryption: 10 | ServerSideEncryptionConfiguration: 11 | - ServerSideEncryptionByDefault: 12 | SSEAlgorithm: AES256 -------------------------------------------------------------------------------- /lab_01_llm_apps_development_tools/lab1.2_s3_cdk/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | -------------------------------------------------------------------------------- /lab_01_llm_apps_development_tools/lab1.2_s3_cdk/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /lab_01_llm_apps_development_tools/lab1.2_s3_cdk/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your CDK TypeScript project 2 | 3 | This is a blank project for CDK development with TypeScript. 4 | 5 | The `cdk.json` file tells the CDK Toolkit how to execute your app. 6 | 7 | ## Useful commands 8 | 9 | * `npm run build` compile typescript to js 10 | * `npm run watch` watch for changes and compile 11 | * `npm run test` perform the jest unit tests 12 | * `npx cdk deploy` deploy this stack to your default AWS account/region 13 | * `npx cdk diff` compare deployed stack with current state 14 | * `npx cdk synth` emits the synthesized CloudFormation template 15 | -------------------------------------------------------------------------------- /lab_01_llm_apps_development_tools/lab1.2_s3_cdk/bin/s3_cdk.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from 'aws-cdk-lib'; 4 | import { S3CdkStack } from '../lib/s3_cdk-stack'; 5 | 6 | const app = new cdk.App(); 7 | new S3CdkStack(app, 'S3CdkStack', { 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 | 12 | /* Uncomment the next line to specialize this stack for the AWS Account 13 | * and Region that are implied by the current CLI configuration. */ 14 | // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, 15 | 16 | /* Uncomment the next line if you know exactly what Account and Region you 17 | * want to deploy the stack to. */ 18 | // env: { account: '123456789012', region: 'us-east-1' }, 19 | 20 | /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ 21 | }); -------------------------------------------------------------------------------- /lab_01_llm_apps_development_tools/lab1.2_s3_cdk/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/s3_cdk.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 33 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 34 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 35 | "@aws-cdk/core:enablePartitionLiterals": true, 36 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 37 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 38 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 39 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 40 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 41 | "@aws-cdk/aws-route53-patters:useCertificate": true, 42 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 43 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 44 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 45 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 46 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 47 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 48 | "@aws-cdk/aws-redshift:columnId": true, 49 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 50 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 51 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 52 | "@aws-cdk/aws-kms:aliasNameRef": true, 53 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 54 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 55 | "@aws-cdk/aws-efs:denyAnonymousAccess": true, 56 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, 57 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, 58 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, 59 | "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, 60 | "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, 61 | "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, 62 | "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, 63 | "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lab_01_llm_apps_development_tools/lab1.2_s3_cdk/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 | -------------------------------------------------------------------------------- /lab_01_llm_apps_development_tools/lab1.2_s3_cdk/lib/s3_cdk-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import * as s3 from 'aws-cdk-lib/aws-s3'; 4 | 5 | export class S3CdkStack extends cdk.Stack { 6 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 7 | super(scope, id, props); 8 | 9 | // Get current AWS account ID 10 | const accountId = cdk.Stack.of(this).account; 11 | 12 | // Get current AWS region 13 | const region = cdk.Stack.of(this).region; 14 | 15 | // Create an S3 bucket with the specified pattern 16 | const bucketName = `genai-bootcamp-cdk-bucket-${accountId}-${region}`; 17 | 18 | // Create an S3 bucket with the current account ID in the name 19 | new s3.Bucket(this, 'MyS3Bucket', { 20 | bucketName: bucketName, 21 | // In production you should update the removal policy to RETAIN to avoid accidental data loss. 22 | removalPolicy: cdk.RemovalPolicy.DESTROY , 23 | encryption: s3.BucketEncryption.KMS_MANAGED 24 | }); 25 | 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lab_01_llm_apps_development_tools/lab1.2_s3_cdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lab1.2_s3_cdk", 3 | "version": "0.1.0", 4 | "bin": { 5 | "lab1.2_s3_cdk": "bin/lab1.2_s3_cdk.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@types/jest": "^29.5.11", 15 | "@types/node": "20.11.14", 16 | "jest": "^29.7.0", 17 | "ts-jest": "^29.1.2", 18 | "aws-cdk": "2.126.0", 19 | "ts-node": "^10.9.2", 20 | "typescript": "~5.3.3" 21 | }, 22 | "dependencies": { 23 | "aws-cdk-lib": "2.126.0", 24 | "constructs": "^10.0.0", 25 | "source-map-support": "^0.5.21" 26 | } 27 | } -------------------------------------------------------------------------------- /lab_01_llm_apps_development_tools/lab1.2_s3_cdk/test/s3_cdk.test.ts: -------------------------------------------------------------------------------- 1 | // import * as cdk from 'aws-cdk-lib'; 2 | // import { Template } from 'aws-cdk-lib/assertions'; 3 | // import * as Lab12S3Cdk from '../lib/lab1.2_s3_cdk-stack'; 4 | 5 | // example test. To run these tests, uncomment this file along with the 6 | // example resource in lib/lab1.2_s3_cdk-stack.ts 7 | test('SQS Queue Created', () => { 8 | // const app = new cdk.App(); 9 | // // WHEN 10 | // const stack = new Lab12S3Cdk.Lab12S3CdkStack(app, 'MyTestStack'); 11 | // // THEN 12 | // const template = Template.fromStack(stack); 13 | 14 | // template.hasResourceProperties('AWS::SQS::Queue', { 15 | // VisibilityTimeout: 300 16 | // }); 17 | }); 18 | -------------------------------------------------------------------------------- /lab_01_llm_apps_development_tools/lab1.2_s3_cdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2020", 7 | "dom" 8 | ], 9 | "declaration": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": false, 19 | "inlineSourceMap": true, 20 | "inlineSources": true, 21 | "experimentalDecorators": true, 22 | "strictPropertyInitialization": false, 23 | "typeRoots": [ 24 | "./node_modules/@types" 25 | ] 26 | }, 27 | "exclude": [ 28 | "node_modules", 29 | "cdk.out" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /lab_02_serverless_llm_assistant_setup/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Goal 3 | 4 | Build a serverless LLM assistant with AWS Lambda and Bedrock using AWS CDK (20min) 5 | 6 | ## Concepts 7 | 8 | In this second lab, you will focus on deploying and interacting with a serverless LLM (Large Language Model) assistant architecture. 9 | First, you will study the architecture diagram, explore the resources described in the CDK stack, 10 | and deploy the serverless LLM assistant infrastructure using CDK commands, 11 | demonstrating the power of Infrastructure as Code (IaC) automation and providing hands-on experience in deploying cloud resources programmatically. 12 | Second, you have the option to deploy a frontend for the LLM assistant using AWS Amplify, 13 | showcasing the integration of a user-friendly web interface with the serverless LLM assistant, 14 | enabling seamless interaction through a graphical user interface (GUI). 15 | Finally, you can interact with the LLM assistant either through the deployed frontend (if installed) or by directly calling the Lambda function, 16 | the former approach providing a user-friendly web interface, while the latter demonstrates interacting with serverless applications programmatically using AWS console or boto3, a Python SDK for AWS. 17 | Throughout the lab, you will gain practical experience in deploying serverless architectures, leveraging IaC automation tools like CDK, 18 | and exploring different methods of interacting with serverless LLM assistants, reinforcing the concepts of serverless computing, IaC automation, and secure access management learned in the previous lab. 19 | 20 | ## Lab 21 | 22 | Follow the instructions below to complete the lab. 23 | 24 | #### Step 1: Understand and deploy the serverless LLM assistant architecture 25 | 26 | 1. Study the architecture diagram for the *serverless llm assistant* below. 27 | * Notice the architecture modules installed at this stage are highlighted in red. 28 | 2. Explore the resources described in the CDK stack and reflect on how they map to the architecture diagram. 29 | 3. Navigate to the `serverless_llm_assistant` directory. 30 | 4. Install the required dependencies by running `npm install`. 31 | 5. Deploy the CDK stack by running `npx cdk deploy`. 32 | 6. Observe and explore the installed resources through the AWS CloudFormation console. 33 | 34 | ![Agentic Assistant workshop Architecture](/assets/agentic-assistant-workshop-architecture-lab-02.png) 35 | 36 | #### Step 2: Deploy the frontend for the LLM assistant 37 | 38 | Installing the frontend is optional, you can interact with assistant through it, or alternatively by directly calling the lambda function through boto3 39 | as suggested in step 3 below. If you want to install the frontend, continue with the steps below otherwise skip to step 3. 40 | 41 | 1. Navigate to the `frontend` directory. 42 | 2. Install the required dependencies by running `npm install`. 43 | 4. Deploy the frontend CI/CD to AWS Amplify by running `npx cdk deploy`. 44 | 5. Go to AWS Amplify [console](https://console.aws.amazon.com/amplify/home) and trigger a build for the app `AmplifyChatUI`. 45 | ![trigger the app build on the Amplify console](/assets/trigger-a-build-of-amplify-app.png) 46 | 6. Access the deployed frontend using the hosting URL. 47 | 48 | #### Step 3: Interact with the LLM assistant 49 | 50 | Now, you can interact with the assistant through a Web UI as follows: 51 | 52 | 1. Access the deployed frontend and authenticate using Cognito. 53 | 2. Interact with the LLM assistant through the UI, using the `basic` assistant mode. 54 | 55 | ![a demonstration of the chat ui](/assets/assistant-ui-demo.png) 56 | 57 | Or by calling the Lambda function directly: 58 | 59 | 1. First install boto3 by running `pip install boto3` in Cloud9 terminal. 60 | 3. Then, using boto3 with the python script `invoke_assistant_lambda.py` you can call the lambda as follows: `python3 invoke_assistant_lambda.py`. You can edit the `user_input` in the script to ask a different question. 61 | 4. Alternatively, you can interact the Lambda function through the [AWS Lambda console](https://console.aws.amazon.com/lambda/home) by passing the following as input. 62 | ```json 63 | { 64 | "session_id": 10, 65 | "user_input": "Could you explain the transformer model?", 66 | "clean_history": true 67 | } 68 | ``` 69 | 70 | At this stage the LLM could only answer questions based on its training data. 71 | For instance, it can assist you in understanding concepts such as the transformer model with the question `Could you explain the transformer model?` 72 | -------------------------------------------------------------------------------- /lab_02_serverless_llm_assistant_setup/invoke_assistant_lambda.py: -------------------------------------------------------------------------------- 1 | import json 2 | import boto3 3 | 4 | # Initialize Boto3 clients for Lambda and SSM 5 | lambda_client = boto3.client("lambda") 6 | ssm_client = boto3.client("ssm") 7 | 8 | 9 | lambda_function_name_ssm_parameter = ( 10 | "/AgenticLLMAssistantWorkshop/AgentExecutorLambdaNameParameter" 11 | ) 12 | 13 | lambda_function_name = ssm_client.get_parameter(Name=lambda_function_name_ssm_parameter) 14 | lambda_function_name = lambda_function_name["Parameter"]["Value"] 15 | 16 | 17 | def call_agent_lambda(user_input, session_id): 18 | payload = { 19 | "user_input": user_input, 20 | "session_id": session_id, 21 | "clean_history": True 22 | } 23 | 24 | try: 25 | # Call the Lambda function 26 | lambda_response = lambda_client.invoke( 27 | FunctionName=lambda_function_name, 28 | InvocationType="RequestResponse", # Use 'Event' for asynchronous invocation 29 | Payload=json.dumps(payload), 30 | ) 31 | 32 | # Parse the Lambda function response 33 | lambda_result = lambda_response["Payload"].read().decode("utf-8") 34 | lambda_result = json.loads(lambda_result) 35 | 36 | return lambda_result 37 | except Exception as e: 38 | print(e) 39 | 40 | 41 | session_id = "10" 42 | user_input = "Could you explain the transformer model?" 43 | 44 | results = call_agent_lambda(user_input, session_id) 45 | 46 | print(results["response"].strip()) 47 | -------------------------------------------------------------------------------- /lab_03_agentic_llm_assistant/README.md: -------------------------------------------------------------------------------- 1 | ## Goal 2 | 3 | Refactor the Serverless LLM Assistant built previously into an agentic, i.e agent based, LLM assistant capable of using a calculator and a search engine to answer questions. 4 | 5 | ## Concepts 6 | 7 | Update the Lambda function to create a custom LLM agent with a calculator and search engine as tools (20min) 8 | 9 | * Here you will continue using the AWS Cloud9 IDE to refactor the AWS lambda function. 10 | * You will understand the difference between a basic LLM assistant vs agentic LLM assistant. 11 | 12 | Follow the instructions below to refactor the basic LLM assistant into an agentic LLM assistant and note the differences. 13 | 14 | ## Lab 15 | 16 | Your task is to update the AWS Lambda function code to implement an agentic LLM assistant. 17 | To achieve this, you will perform the following steps: 18 | 19 | 1. Identify the different components of the agent: agent executor, agent, agent prompt, agent tools 20 | 2. Write the agent prompt 21 | 3. Define the agent tools 22 | 4. Create the agent and agent executor 23 | 24 | Throughout this lab, you will focus on the resources highlighted in red below, namely the Lambda function which serves as the orchestrator of the LLM assistant. 25 | 26 | ![Agentic Assistant workshop Architecture](/assets/agentic-assistant-workshop-architecture-lab-03.png) 27 | 28 | #### Step 1: List and discuss agent components 29 | 30 | Explained in the slides. 31 | 32 | #### Step 2: Write the agent prompt 33 | 34 | The agent behaviour relies on leveraging an LLM with the Reason and Act (ReAct) instruction format to define a specific prompt that triggers the LLM to decide what action it needs to take next among a list of options, 35 | and whether it has enough information to answer a given question after a series of actions. 36 | 37 | Your tasks is to refactor the AWS Lambda function to add the ReAct prompt following the instructions below: 38 | 39 | 1. Open the file `serverless_llm_assistant/lib/lambda-functions/agent-executor-lambda-container/agent-executor-lambda/assistant/prompts.py` in your editor. This file contain the prompt of the basic LLM assistant currently used in the AWS Lambda function 40 | 2. Add the following code which defines an LLM agent prompt with the ReAct instruction format 41 | 42 | ```python 43 | # ============================================================================ 44 | # Claude agent prompt construction 45 | # ============================================================================ 46 | # Inspired by and adapted from 47 | # https://python.langchain.com/docs/modules/agents/how_to/custom_llm_agent 48 | 49 | CLAUDE_AGENT_PROMPT_TEMPLATE = f"""\n 50 | Human: The following is a conversation between a human and an AI assistant. 51 | The assistant is polite, and responds to the user input and questions acurately and concisely. 52 | The assistant remains on the topic and leverage available options efficiently. 53 | The date today is {date_today}. 54 | 55 | You will play the role of the assistant. 56 | You have access to the following tools: 57 | 58 | {{tools}} 59 | 60 | You must reason through the question using the following format: 61 | 62 | Question: The question found below which you must answer 63 | Thought: you should always think about what to do 64 | Action: the action to take, must be one of [{{tool_names}}] 65 | Action Input: the input to the action 66 | Observation: the result of the action 67 | ... (this Thought/Action/Action Input/Observation can repeat N times) 68 | Thought: I now know the final answer 69 | Final Answer: the final answer to the original input question 70 | 71 | Remember to respond with your knowledge when the question does not correspond to any available action. 72 | 73 | The conversation history is within the XML tags below, where Hu refers to human and AI refers to the assistant: 74 | 75 | {{chat_history}} 76 | 77 | 78 | Begin! 79 | 80 | Question: {{input}} 81 | 82 | Assistant: 83 | {{agent_scratchpad}} 84 | """ 85 | 86 | CLAUDE_AGENT_PROMPT = PromptTemplate.from_template( 87 | CLAUDE_AGENT_PROMPT_TEMPLATE 88 | ) 89 | ``` 90 | 3. Discuss the structure of this prompt with your instructor, namely: 91 | * How the tools are included. 92 | * How the ReAct instruction format looks like 93 | 94 | #### Step 3: Define the agent tools 95 | 96 | Here you will define what the actual tools are and how they will be integrated to the agent. 97 | 98 | 1. Open the file `serverless_llm_assistant/lib/lambda-functions/agent-executor-lambda-container/agent-executor-lambda/assistant/tools.py` in your editor. In this file, you will add the tools definition. 99 | 2. Add the following tools definition to extend the agent with ability to search with `search engine` and to do math with a dedicated `calculator` 100 | ```python 101 | search = DuckDuckGoSearchRun() 102 | custom_calculator = CustomCalculatorTool() 103 | 104 | LLM_AGENT_TOOLS = [ 105 | Tool( 106 | name="Search", 107 | func=search.invoke, 108 | description=( 109 | "Use when you need to answer questions about current events, news or people." 110 | " You should ask targeted questions." 111 | ), 112 | ), 113 | Tool( 114 | name="Calculator", 115 | func=custom_calculator, 116 | description=( 117 | "Always Use this tool when you need to answer math questions." 118 | " The input to Calculator can only be a valid math expression, such as 55/3." 119 | ), 120 | ), 121 | ] 122 | ``` 123 | 3. Dicuss the tools with your instructor, namely: 124 | * The importance of the description in helping the agent identify the right tool for a specific task. 125 | 126 | 127 | #### Step 4: Create the agent executor 128 | 129 | Now, to bring everything together and have the agent assistant ready to be used, we need to create an agent executor. 130 | In fact, the agent executor is a piece of deterministic python code that orchestrates how everything works together. 131 | It triggers the calls to the LLM, and parse out its output using regular expression to match with the correct next action to take 132 | based on the available tools names. To build this agent executor, we can rely on the langchain class `langchain.agents.AgentExecutor`. 133 | 134 | 135 | 1. To add the agent executor, open the lambda handler file in your editor from `serverless_llm_assistant/lib/lambda-functions/agent-executor-lambda-container/agent-executor-lambda/handler.py`. 136 | 2. update the file header to import the necessary dependencies by adding the following import statements: 137 | ```python 138 | from langchain.agents import AgentExecutor, create_react_agent 139 | from assistant.prompts import CLAUDE_AGENT_PROMPT 140 | from assistant.tools import LLM_AGENT_TOOLS 141 | ``` 142 | 3. Then, add the following helper method to the handler to create an instance of the agent executor with the correct setup: 143 | ```python 144 | def get_agentic_chatbot_conversation_chain( 145 | user_input, session_id, clean_history, verbose=True 146 | ): 147 | message_history = DynamoDBChatMessageHistory( 148 | table_name=config.chat_message_history_table_name, session_id=session_id 149 | ) 150 | if clean_history: 151 | message_history.clear() 152 | 153 | memory = ConversationBufferMemory( 154 | memory_key="chat_history", 155 | chat_memory=message_history, 156 | ai_prefix="AI", 157 | # change the human_prefix from Human to something else 158 | # to not conflict with Human keyword in Anthropic Claude model. 159 | human_prefix="Hu", 160 | return_messages=False, 161 | ) 162 | 163 | agent = create_react_agent( 164 | llm=claude_llm, 165 | tools=LLM_AGENT_TOOLS, 166 | prompt=CLAUDE_AGENT_PROMPT, 167 | ) 168 | 169 | agent_chain = AgentExecutor.from_agent_and_tools( 170 | agent=agent, 171 | tools=LLM_AGENT_TOOLS, 172 | verbose=verbose, 173 | memory=memory, 174 | handle_parsing_errors="Check your output and make sure it conforms!", 175 | ) 176 | return agent_chain 177 | ``` 178 | 4. Finally, update the lambda `lambda_handler` function inside the same lambda `handler.py` file to expose the new agentic chat mode to users. To do this, replace the `lambda_handler` with the following 179 | ```python 180 | def lambda_handler(event, context): 181 | logger.info(event) 182 | user_input = event["user_input"] 183 | session_id = event["session_id"] 184 | chatbot_type = event.get("chatbot_type", "basic") 185 | chatbot_types = ["basic", "agentic"] 186 | clean_history = event.get("clean_history", False) 187 | 188 | if chatbot_type == "basic": 189 | conversation_chain = get_basic_chatbot_conversation_chain( 190 | user_input, session_id, clean_history 191 | ).invoke 192 | elif chatbot_type == "agentic": 193 | conversation_chain = get_agentic_chatbot_conversation_chain( 194 | user_input, session_id, clean_history 195 | ).invoke 196 | else: 197 | return { 198 | "statusCode": 200, 199 | "response": ( 200 | f"The chatbot_type {chatbot_type} is not supported." 201 | f" Please use one of the following types: {chatbot_types}" 202 | ), 203 | } 204 | 205 | try: 206 | response = conversation_chain({"input": user_input}) 207 | 208 | if chatbot_type == "basic": 209 | response = response["response"] 210 | elif chatbot_type == "agentic": 211 | response = response["output"] 212 | 213 | except Exception: 214 | response = ( 215 | "Unable to respond due to an internal issue." 216 | " Please try again later" 217 | ) 218 | print(traceback.format_exc()) 219 | 220 | return {"statusCode": 200, "response": response} 221 | ``` 222 | 223 | Now, run `npx cdk deploy` again to deploy the changes. Then interact with the Lambda function by asking questions and observing the behavior. -------------------------------------------------------------------------------- /lab_05_agent_with_analytics/README.md: -------------------------------------------------------------------------------- 1 | ## Goal 2 | 3 | Extend the agent further with the ability to answer analytical queries. This will leverage the Text-to-SQL design pattern. 4 | 5 | ## Concepts 6 | 7 | * Using LLM for generating Text-to-SQL 8 | * Guidelines for LLM prompts to generate good Text-to-SQL. 9 | 10 | ## Lab 11 | 12 | In this lab, we will further extend the agent with the ability to answer analytical questions, such as `what is the most profitable region?`. 13 | To achieve this, you will work on adding the ability to translate user questions into SQL queries, validating and running these queries against a SQL database, 14 | and finally integrating the results with the agent to answer analytical questions. 15 | 16 | Throughout this lab, you will focus on the resources highlighted in red below, namely updating the LLM agent in AWS Lambda function and ingesting a SQL table into the database. 17 | 18 | ![Agentic Assistant workshop Architecture](/assets/agentic-assistant-workshop-architecture-lab-05.png) 19 | 20 | #### Step 1: Load a SQL table to the PostgreSQL DB 21 | 22 | Run the notebook `data_pipelines/03-load-sql-tables-into-aurora-postgreSQL.ipynb` which will be used to generate a synthetic SQL table (todo) and load it into the Aurora PostgreSQL DB. 23 | 24 | #### Step 2: Extend the agent with a tool for analytical question answering with SQL 25 | 26 | Adding the ability to answer analytical queries entails: 27 | 28 | 1. Converting user questions accurately into SQL queries. 29 | 2. Validating and executing the user queries against the DB. 30 | 3. Augmenting the LLM with the results of the SQL query so it is able to formulate an answer to the question. 31 | 32 | Your task is to implement this by following the instructions below. 33 | 34 | 1. Study the `sqlqa.py` file, namely the `get_sql_qa_tool` function which connects everything together, and the LLM prompt template defined in `_SQL_TEMPLATE`. 35 | 2. Reflect, and discuss if possible, the different inputs used within this prompt to generate the SQL query, and potential risks and mitigations. 36 | 3. Add the relevant imports below, and create an instance of the `get_text_to_sql_chain`. 37 | 38 | ```python 39 | from .sqlqa import get_sql_qa_tool, get_text_to_sql_chain 40 | 41 | ... 42 | TEXT_TO_SQL_CHAIN = get_text_to_sql_chain(config, claude_llm) 43 | ``` 44 | 45 | 4. Then add the tool definition to the list of tools. Feel free to tweak the description to help the LLM pick this tool and improve it. 46 | 47 | ```python 48 | Tool( 49 | name="SQLQA", 50 | func=lambda question: get_sql_qa_tool(question, TEXT_TO_SQL_CHAIN), 51 | description=( 52 | "Use when you are asked analytical questions about financial reports of companies." 53 | " For example, when asked to give the average or maximum revenue of a company, etc." 54 | " The input should be a targeted question." 55 | ), 56 | ), 57 | ``` 58 | 59 | Now redeploy the stack with `npx cdk deploy` 60 | 61 | #### Step 3: Load structured metadata into SQL tables 62 | 63 | Run the notebook `data_pipelines/05-load-sql-tables-into-aurora-postgreSQL.ipynb` to load the structured metadata extracted from the PDF documents into SQL tables in the PostgreSQL database. This will enable the agent to answer analytical questions based on the structured data. 64 | 65 | #### Step 4: Interact with the Assistant and test the SQLQA tool 66 | 67 | Now, you can interact with the Assistant by asking analytical questions related to the financial reports and structured data you've loaded into the database. The Assistant should be able to leverage the SQLQA tool to translate your questions into SQL queries, execute them against the database, and incorporate the results into its responses. 68 | 69 | Here are some examples of analytical questions you could ask: 70 | 71 | * What was the region with the highest revenue for Amazon in 2022? 72 | * How has Amazon's research and development spending changed over the past 5 years? 73 | * Which product category has the highest profit margin for Amazon? 74 | * What is the average customer acquisition cost for Amazon Prime subscribers? 75 | * How does Amazon's revenue growth compare to its competitors in the e-commerce industry? 76 | 77 | When asking these questions, observe how the Assistant utilizes the SQLQA tool and incorporates the results from the database into its responses. If the Assistant struggles to understand or answer the analytical questions correctly, you may need to refine the prompts or provide additional guidance to improve the Text-to-SQL capability. -------------------------------------------------------------------------------- /serverless_llm_assistant/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | -------------------------------------------------------------------------------- /serverless_llm_assistant/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /serverless_llm_assistant/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your CDK TypeScript project 2 | 3 | This is a blank project for CDK development with TypeScript. 4 | 5 | The `cdk.json` file tells the CDK Toolkit how to execute your app. 6 | 7 | ## Useful commands 8 | 9 | * `npm run build` compile typescript to js 10 | * `npm run watch` watch for changes and compile 11 | * `npm run test` perform the jest unit tests 12 | * `npx cdk deploy` deploy this stack to your default AWS account/region 13 | * `npx cdk diff` compare deployed stack with current state 14 | * `npx cdk synth` emits the synthesized CloudFormation template 15 | -------------------------------------------------------------------------------- /serverless_llm_assistant/bin/serverless_llm_assistant.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from 'aws-cdk-lib'; 4 | import { ServerlessLlmAssistantStack } from '../lib/serverless_llm_assistant-stack'; 5 | 6 | const app = new cdk.App(); 7 | new ServerlessLlmAssistantStack(app, 'ServerlessLlmAssistantStack', { 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 | 12 | /* Uncomment the next line to specialize this stack for the AWS Account 13 | * and Region that are implied by the current CLI configuration. */ 14 | // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, 15 | 16 | /* Uncomment the next line if you know exactly what Account and Region you 17 | * want to deploy the stack to. */ 18 | // env: { account: '123456789012', region: 'us-east-1' }, 19 | 20 | /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ 21 | }); -------------------------------------------------------------------------------- /serverless_llm_assistant/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/serverless_llm_assistant.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 33 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 34 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 35 | "@aws-cdk/core:enablePartitionLiterals": true, 36 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 37 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 38 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 39 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 40 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 41 | "@aws-cdk/aws-route53-patters:useCertificate": true, 42 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 43 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 44 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 45 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 46 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 47 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 48 | "@aws-cdk/aws-redshift:columnId": true, 49 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 50 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 51 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 52 | "@aws-cdk/aws-kms:aliasNameRef": true, 53 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 54 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 55 | "@aws-cdk/aws-efs:denyAnonymousAccess": true, 56 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, 57 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, 58 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, 59 | "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, 60 | "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, 61 | "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, 62 | "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, 63 | "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /serverless_llm_assistant/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 | -------------------------------------------------------------------------------- /serverless_llm_assistant/lib/assistant-api-gateway.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import * as apigateway from 'aws-cdk-lib/aws-apigateway'; 3 | import * as lambda from 'aws-cdk-lib/aws-lambda'; 4 | import * as cognito from 'aws-cdk-lib/aws-cognito'; 5 | import * as ssm from 'aws-cdk-lib/aws-ssm'; 6 | import { Construct } from 'constructs'; 7 | 8 | interface AssistantApiConstructProps { 9 | cognitoUserPool: cognito.UserPool; 10 | lambdaFunction: lambda.Function; 11 | } 12 | 13 | export class AssistantApiConstruct extends Construct { 14 | public readonly api: apigateway.RestApi; 15 | 16 | constructor(scope: Construct, id: string, props: AssistantApiConstructProps) { 17 | super(scope, id); 18 | 19 | const { cognitoUserPool, lambdaFunction } = props; 20 | 21 | this.api = new apigateway.RestApi(this, 'AssistantApi', { 22 | restApiName: 'assistant-api', 23 | description: 24 | 'An API to invoke an LLM based agent which orchestrates using tools to answer user questions.', 25 | defaultCorsPreflightOptions: { 26 | // // Change this to the specific origin of your app in production 27 | allowOrigins: apigateway.Cors.ALL_ORIGINS, 28 | allowMethods: apigateway.Cors.ALL_METHODS, 29 | allowHeaders: ['Content-Type', 'Authorization'], 30 | }, 31 | }); 32 | 33 | // https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.CognitoUserPoolsAuthorizer.html 34 | const cognitoAuthorizer = new apigateway.CognitoUserPoolsAuthorizer(this, 'ChatAuthorizer', { 35 | cognitoUserPools: [cognitoUserPool], 36 | }); 37 | 38 | 39 | const lambdaIntegration = new apigateway.LambdaIntegration(lambdaFunction, { 40 | proxy: false, 41 | integrationResponses: [ 42 | { 43 | statusCode: '200', 44 | // Enable CORS for the Lambda Integration 45 | responseParameters: { 46 | 'method.response.header.Access-Control-Allow-Headers': "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'", 47 | 'method.response.header.Access-Control-Allow-Origin': "'*'", 48 | 'method.response.header.Access-Control-Allow-Methods': "'POST,OPTIONS'", 49 | }, 50 | }, 51 | ], 52 | }); 53 | 54 | this.api.root.addMethod( 55 | 'POST', 56 | lambdaIntegration, 57 | { 58 | methodResponses: [ 59 | { 60 | statusCode: '200', 61 | responseParameters: { 62 | 'method.response.header.Access-Control-Allow-Headers': true, 63 | 'method.response.header.Access-Control-Allow-Origin': true, 64 | 'method.response.header.Access-Control-Allow-Methods': true, 65 | }, 66 | }, 67 | ], 68 | authorizer: cognitoAuthorizer, 69 | authorizationType: apigateway.AuthorizationType.COGNITO, 70 | } 71 | ); 72 | 73 | // Add an SSM parameter to hold Rest API URL 74 | new ssm.StringParameter( 75 | this, 76 | "AgentAPIURLParameter", 77 | { 78 | parameterName: "/AgenticLLMAssistantWorkshop/agent_api", 79 | stringValue: this.api.url 80 | } 81 | ); 82 | 83 | // stack output 84 | new cdk.CfnOutput(this, "EndpointURL", { 85 | value: this.api.url 86 | }); 87 | } 88 | } -------------------------------------------------------------------------------- /serverless_llm_assistant/lib/assistant-authorizer.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import * as cognito from 'aws-cdk-lib/aws-cognito'; 3 | import * as ssm from 'aws-cdk-lib/aws-ssm'; 4 | import { Construct } from 'constructs'; 5 | 6 | interface CognitoConstructProps { 7 | userPoolName?: string; 8 | clientName?: string; 9 | } 10 | 11 | export class CognitoConstruct extends Construct { 12 | public readonly userPool: cognito.UserPool; 13 | public readonly userPoolClient: cognito.UserPoolClient; 14 | 15 | constructor(scope: Construct, id: string, props?: CognitoConstructProps) { 16 | super(scope, id); 17 | 18 | // Create a new Cognito user pool 19 | // documentation: https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cognito.UserPool.html 20 | this.userPool = new cognito.UserPool(this, 'CognitoPool', { 21 | autoVerify: { email: true }, 22 | removalPolicy: cdk.RemovalPolicy.DESTROY, 23 | selfSignUpEnabled: true, 24 | signInCaseSensitive: false, 25 | signInAliases: { 26 | email: true, 27 | username: true, 28 | }, 29 | standardAttributes: { 30 | email: { 31 | required: true, 32 | mutable: false, 33 | }, 34 | }, 35 | }); 36 | 37 | // Add an app client to the user pool 38 | this.userPoolClient = this.userPool.addClient('NextJsAppClient', { 39 | oAuth: { 40 | flows: { 41 | authorizationCodeGrant: true, 42 | }, 43 | scopes: [cognito.OAuthScope.OPENID], 44 | callbackUrls: ['https://localhost:3000/'], 45 | logoutUrls: ['https://localhost:3000/'], 46 | }, 47 | }); 48 | 49 | // Add an SSM parameter to hold the cognito user pool id 50 | new ssm.StringParameter( 51 | this, 52 | "cognitoUserPoolParameter", 53 | { 54 | parameterName: "/AgenticLLMAssistantWorkshop/cognito_user_pool_id", 55 | stringValue: this.userPool.userPoolId, 56 | } 57 | ); 58 | 59 | // Add an SSM parameter to hold the cognito user pool id 60 | new ssm.StringParameter( 61 | this, 62 | "cognitoUserPoolClientParameter", 63 | { 64 | parameterName: "/AgenticLLMAssistantWorkshop/cognito_user_pool_client_id", 65 | stringValue: this.userPoolClient.userPoolClientId, 66 | } 67 | ); 68 | 69 | // Stack outputs 70 | new cdk.CfnOutput(this, "UserPoolClient", { 71 | value: this.userPoolClient.userPoolClientId, 72 | }); 73 | 74 | new cdk.CfnOutput(this, "UserPoolId", { 75 | value: this.userPool.userPoolId 76 | }); 77 | 78 | new cdk.CfnOutput(this, "UserPoolProviderURL", { 79 | value: this.userPool.userPoolProviderUrl 80 | }); 81 | 82 | } 83 | } -------------------------------------------------------------------------------- /serverless_llm_assistant/lib/assistant-sagemaker-iam-policy.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 rds from 'aws-cdk-lib/aws-rds'; 5 | import * as s3 from 'aws-cdk-lib/aws-s3'; 6 | import * as ssm from 'aws-cdk-lib/aws-ssm'; 7 | import { Construct } from 'constructs'; 8 | import { SageMakerRdsAccessConstruct } from './assistant-sagemaker-postgres-acess'; 9 | 10 | interface SageMakerIAMPolicyConstructProps { 11 | bedrockRegionParameter: ssm.StringParameter; 12 | llmModelIdParameter: ssm.StringParameter; 13 | agentDataBucketParameter: ssm.StringParameter; 14 | agentLambdaNameParameter: ssm.StringParameter; 15 | agentDataBucket: s3.Bucket; 16 | agentExecutorLambda: lambda.Function; 17 | rdsCluster: rds.DatabaseCluster; 18 | sagemaker_rds_access: SageMakerRdsAccessConstruct; 19 | } 20 | 21 | export class SageMakerIAMPolicyConstruct extends Construct { 22 | public readonly sageMakerPostgresDBAccessIAMPolicy: iam.ManagedPolicy; 23 | 24 | constructor(scope: Construct, id: string, props: SageMakerIAMPolicyConstructProps) { 25 | super(scope, id); 26 | 27 | this.sageMakerPostgresDBAccessIAMPolicy = new iam.ManagedPolicy(this, 'SageMakerPostgresDBAccessIAMPolicy', { 28 | statements: [ 29 | new iam.PolicyStatement({ 30 | actions: ['ssm:GetParameter'], 31 | resources: [ 32 | props.bedrockRegionParameter.parameterArn, 33 | props.llmModelIdParameter.parameterArn, 34 | props.agentDataBucketParameter.parameterArn, 35 | props.agentLambdaNameParameter.parameterArn, 36 | props.sagemaker_rds_access.processingSecurityGroupIdParameter.parameterArn, 37 | props.sagemaker_rds_access.dbSecretArnParameter.parameterArn, 38 | props.sagemaker_rds_access.subnetIdsParameter.parameterArn, 39 | ], 40 | }), 41 | new iam.PolicyStatement({ 42 | actions: ['s3:GetObject', 's3:PutObject', 's3:DeleteObject', 's3:ListBucket'], 43 | resources: [props.agentDataBucket.bucketArn, props.agentDataBucket.arnForObjects('*')], 44 | }), 45 | new iam.PolicyStatement({ 46 | actions: ['lambda:InvokeFunction'], 47 | resources: [props.agentExecutorLambda.functionArn], 48 | }), 49 | new iam.PolicyStatement({ 50 | actions: ['secretsmanager:GetSecretValue'], 51 | resources: [props.rdsCluster.secret?.secretArn as string], 52 | }), 53 | ], 54 | }); 55 | 56 | new cdk.CfnOutput(this, "sageMakerPostgresDBAccessIAMPolicyARN", { 57 | value: this.sageMakerPostgresDBAccessIAMPolicy.managedPolicyArn, 58 | }); 59 | } 60 | } -------------------------------------------------------------------------------- /serverless_llm_assistant/lib/assistant-sagemaker-postgres-acess.ts: -------------------------------------------------------------------------------- 1 | import * as ec2 from 'aws-cdk-lib/aws-ec2'; 2 | import * as rds from 'aws-cdk-lib/aws-rds'; 3 | import * as ssm from 'aws-cdk-lib/aws-ssm'; 4 | import { Construct } from 'constructs'; 5 | 6 | interface SageMakerRdsAccessConstructProps { 7 | vpc: ec2.Vpc; 8 | rdsCluster: rds.DatabaseCluster; 9 | } 10 | 11 | export class SageMakerRdsAccessConstruct extends Construct { 12 | public readonly processingSecurityGroup: ec2.SecurityGroup; 13 | public readonly processingSecurityGroupIdParameter: ssm.StringParameter; 14 | public readonly dbSecretArnParameter: ssm.StringParameter; 15 | public readonly subnetIdsParameter: ssm.StringParameter; 16 | 17 | constructor(scope: Construct, id: string, props: SageMakerRdsAccessConstructProps) { 18 | super(scope, id); 19 | 20 | // Create a security group to allow access to the DB from a SageMaker processing job 21 | this.processingSecurityGroup = new ec2.SecurityGroup(this, 'ProcessingSecurityGroup', { 22 | vpc: props.vpc, 23 | allowAllOutbound: true, // Allow outbound traffic to the Aurora security group on PostgreSQL port 24 | }); 25 | 26 | // Allow connection to SageMaker processing jobs security group on the specified port 27 | props.rdsCluster.connections.allowTo( 28 | this.processingSecurityGroup, 29 | ec2.Port.tcp(5432), 30 | 'Allow outbound traffic to RDS from SageMaker jobs' 31 | ); 32 | 33 | // Allow inbound traffic to the RDS security group from the SageMaker processing security group 34 | props.rdsCluster.connections.allowFrom( 35 | this.processingSecurityGroup, 36 | ec2.Port.tcp(5432), 37 | 'Allow inbound traffic from SageMaker to RDS' 38 | ); 39 | 40 | // Store the security group ID in Parameter Store 41 | this.processingSecurityGroupIdParameter = new ssm.StringParameter(this, 'ProcessingSecurityGroupIdParameter', { 42 | parameterName: '/AgenticLLMAssistantWorkshop/SMProcessingJobSecurityGroupId', 43 | stringValue: this.processingSecurityGroup.securityGroupId, 44 | }); 45 | 46 | // Save the required credentials and parameter that would allow SageMaker Jobs 47 | // to access the database and add the required IAM permissions to a managed 48 | // IAM policy that must be attached to the SageMaker execution role. 49 | this.dbSecretArnParameter = new ssm.StringParameter(this, 'DBSecretArnParameter', { 50 | parameterName: '/AgenticLLMAssistantWorkshop/DBSecretARN', 51 | stringValue: props.rdsCluster.secret?.secretArn as string, 52 | }); 53 | 54 | // Retrieve the subnet IDs from the VPC 55 | const subnetIds = props.vpc.selectSubnets({ 56 | subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, 57 | }).subnetIds; 58 | 59 | // Convert the subnet IDs to a JSON format 60 | const subnetIdsJson = JSON.stringify(subnetIds); 61 | 62 | // Store the JSON data as an SSM parameter 63 | this.subnetIdsParameter = new ssm.StringParameter(this, 'SubnetIdsParameter', { 64 | parameterName: '/AgenticLLMAssistantWorkshop/SubnetIds', 65 | stringValue: subnetIdsJson, 66 | }); 67 | 68 | } 69 | } -------------------------------------------------------------------------------- /serverless_llm_assistant/lib/assistant-sagemaker-processor.ts: -------------------------------------------------------------------------------- 1 | import { DockerImageAsset, NetworkMode} from 'aws-cdk-lib/aws-ecr-assets'; 2 | import { Construct } from 'constructs'; 3 | import { CfnOutput } from 'aws-cdk-lib'; 4 | import * as ssm from "aws-cdk-lib/aws-ssm"; 5 | 6 | 7 | export class SageMakerProcessor extends Construct { 8 | public image: DockerImageAsset; 9 | 10 | constructor(scope: Construct, id: string) { 11 | super(scope, id); 12 | 13 | const processor_image = new DockerImageAsset(this, "processor_image", { 14 | assetName: "processor", 15 | directory: "../data_pipelines/docker", 16 | networkMode: NetworkMode.custom("sagemaker") 17 | }) 18 | 19 | this.image = processor_image 20 | 21 | new ssm.StringParameter( 22 | this, 23 | "ScriptProcessorContainerParameter", 24 | { 25 | parameterName: "/AgenticLLMAssistantWorkshop/ScriptProcessorContainer", 26 | stringValue: this.image.imageUri 27 | } 28 | ); 29 | 30 | new CfnOutput(this, "i", { 31 | description: "ScriptProcessorImageURI", 32 | value: this.image.imageUri 33 | }) 34 | 35 | } 36 | } -------------------------------------------------------------------------------- /serverless_llm_assistant/lib/assistant-vpc.ts: -------------------------------------------------------------------------------- 1 | // Credit goes to -> https://github.com/aws-samples/aws-genai-llm-chatbot/blob/main/lib/vpc/index.ts 2 | import * as ec2 from 'aws-cdk-lib/aws-ec2'; 3 | import { Construct } from 'constructs'; 4 | 5 | export class Vpc extends Construct { 6 | public readonly vpc: ec2.Vpc; 7 | public readonly s3GatewayEndpoint: ec2.IGatewayVpcEndpoint; 8 | public readonly s3vpcEndpoint: ec2.IGatewayVpcEndpoint; 9 | public readonly dynamodbvpcEndpoint: ec2.IGatewayVpcEndpoint; 10 | public readonly secretsManagerVpcEndpoint: ec2.IInterfaceVpcEndpoint; 11 | public readonly apiGatewayVpcEndpoint: ec2.IInterfaceVpcEndpoint; 12 | 13 | constructor(scope: Construct, id: string) { 14 | super(scope, id); 15 | 16 | const vpc = new ec2.Vpc(this, 'VPC', { 17 | maxAzs: 2, 18 | subnetConfiguration: [ 19 | { 20 | name: 'public', 21 | subnetType: ec2.SubnetType.PUBLIC, 22 | }, 23 | { 24 | name: 'private', 25 | subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, 26 | }, 27 | { 28 | name: 'isolated', 29 | subnetType: ec2.SubnetType.PRIVATE_ISOLATED, 30 | }, 31 | ], 32 | }); 33 | 34 | // Create a VPC endpoint for S3. 35 | this.s3GatewayEndpoint = vpc.addGatewayEndpoint('S3GatewayEndpoint', { 36 | service: ec2.GatewayVpcEndpointAwsService.S3, 37 | }); 38 | 39 | this.s3vpcEndpoint = vpc.addInterfaceEndpoint('S3InterfaceEndpoint', { 40 | service: ec2.InterfaceVpcEndpointAwsService.S3, 41 | open: true, 42 | }); 43 | this.s3vpcEndpoint.node.addDependency(this.s3GatewayEndpoint); 44 | 45 | // Create a VPC endpoint for DynamoDB. 46 | this.dynamodbvpcEndpoint = vpc.addGatewayEndpoint('DynamoDBEndpoint', { 47 | service: ec2.GatewayVpcEndpointAwsService.DYNAMODB, 48 | }); 49 | 50 | // Create VPC Endpoint for Secrets Manager 51 | this.secretsManagerVpcEndpoint = vpc.addInterfaceEndpoint('SecretsManagerEndpoint', { 52 | service: ec2.InterfaceVpcEndpointAwsService.SECRETS_MANAGER, 53 | open: true, 54 | }); 55 | 56 | this.vpc = vpc; 57 | } 58 | } -------------------------------------------------------------------------------- /serverless_llm_assistant/lib/lambda-functions/agent-executor-lambda-container/Dockerfile: -------------------------------------------------------------------------------- 1 | # stage 1: build the lambda layer 2 | FROM --platform=linux/x86_64 public.ecr.aws/lambda/python:3.12 AS builder 3 | 4 | # Copy requirements.txt 5 | COPY requirements.txt /tmp/ 6 | 7 | # Install the specified packages with --no-cache-dir 8 | # to avoid increasing the container size with caching within the container. 9 | RUN pip install --upgrade pip 10 | RUN pip install --force-reinstall --no-cache-dir \ 11 | -r /tmp/requirements.txt -t /build/python 12 | 13 | # Install 'zip' utility (Amazon Linux uses yum) 14 | RUN dnf install -y zip 15 | 16 | # Create the ZIP file for the Lambda layer 17 | RUN cd /build/python && zip -r /opt/bedrock_layer.zip . 18 | 19 | # Stage 2: Build the Lambda function 20 | FROM --platform=linux/x86_64 public.ecr.aws/lambda/python:3.12 21 | 22 | # Copy the Lambda layer artifacts from the builder stage 23 | COPY --from=builder /opt/bedrock_layer.zip /opt/ 24 | COPY --from=builder /build/python /opt/python 25 | 26 | # Copy function code 27 | COPY agent-executor-lambda ${LAMBDA_TASK_ROOT} 28 | 29 | # Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile) 30 | CMD [ "handler.lambda_handler" ] -------------------------------------------------------------------------------- /serverless_llm_assistant/lib/lambda-functions/agent-executor-lambda-container/agent-executor-lambda/assistant/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/build-an-agentic-llm-assistant/1e4b2bea103b0af9b4f9aeffa620231dc1867002/serverless_llm_assistant/lib/lambda-functions/agent-executor-lambda-container/agent-executor-lambda/assistant/__init__.py -------------------------------------------------------------------------------- /serverless_llm_assistant/lib/lambda-functions/agent-executor-lambda-container/agent-executor-lambda/assistant/calculator.py: -------------------------------------------------------------------------------- 1 | # The custom calculator is built to avoid the issue 2 | # https://github.com/langchain-ai/langchain/issues/3071 3 | # Inspiration sources: 4 | # https://python.langchain.com/docs/modules/agents/tools/custom_tools 5 | # and (https://github.com/langchain-ai/langchain/blob/master/libs/ 6 | # langchain/langchain/chains/llm_math/base.py#L82) 7 | 8 | import math 9 | from typing import Optional, Type 10 | 11 | from langchain.callbacks.manager import (AsyncCallbackManagerForToolRun, 12 | CallbackManagerForToolRun) 13 | from langchain.tools import BaseTool 14 | from pydantic import BaseModel, Field 15 | 16 | 17 | class CalculatorInput(BaseModel): 18 | question: str = Field() 19 | 20 | 21 | def _evaluate_expression(expression: str) -> str: 22 | import numexpr # noqa: F401 23 | 24 | try: 25 | local_dict = {"pi": math.pi, "e": math.e} 26 | output = str( 27 | numexpr.evaluate( 28 | expression.strip(), 29 | global_dict={}, # restrict access to globals 30 | local_dict=local_dict, # add common mathematical functions 31 | ) 32 | ) 33 | except Exception as e: 34 | raise ValueError( 35 | f'LLMMathChain._evaluate("{expression}") raised error: {e}.' 36 | " Please try again with a valid numerical expression" 37 | ) 38 | 39 | return output.strip() 40 | 41 | 42 | class CustomCalculatorTool(BaseTool): 43 | name = "Calculator" 44 | description = "useful for when you need to answer questions about math" 45 | args_schema: Type[BaseModel] = CalculatorInput 46 | 47 | def _run( 48 | self, query: str, run_manager: Optional[CallbackManagerForToolRun] = None 49 | ) -> str: 50 | """Use the tool.""" 51 | try: 52 | return _evaluate_expression(query.strip()) 53 | except Exception as e: 54 | return ( 55 | f"Failed to evaluate the expression with error {e}." 56 | " Please only provide a valid math expression." 57 | ) 58 | 59 | async def _arun( 60 | self, query: str, run_manager: Optional[AsyncCallbackManagerForToolRun] = None 61 | ) -> str: 62 | """Use the tool asynchronously.""" 63 | raise NotImplementedError("Calculator does not support async") -------------------------------------------------------------------------------- /serverless_llm_assistant/lib/lambda-functions/agent-executor-lambda-container/agent-executor-lambda/assistant/config.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from dataclasses import dataclass 4 | 5 | import boto3 6 | from langchain_community.vectorstores import PGVector 7 | from langchain_community.utilities import SQLDatabase 8 | import sqlalchemy 9 | 10 | ssm = boto3.client("ssm") 11 | secretsmanager_client = boto3.client("secretsmanager") 12 | 13 | # TODO: put in parameter store and read with a default factory in the dataclass 14 | SQL_TABLE_NAMES = ["extracted_entities"] 15 | 16 | @dataclass 17 | class AgenticAssistantConfig: 18 | bedrock_region: str = ssm.get_parameter( 19 | Name=os.environ["BEDROCK_REGION_PARAMETER"] 20 | )["Parameter"]["Value"] 21 | 22 | llm_model_id: str = ssm.get_parameter(Name=os.environ["LLM_MODEL_ID_PARAMETER"])[ 23 | "Parameter" 24 | ]["Value"] 25 | 26 | chat_message_history_table_name: str = os.environ["CHAT_MESSAGE_HISTORY_TABLE"] 27 | agent_db_secret_id: str = os.environ.get("AGENT_DB_SECRET_ID", "NOSECRET") 28 | 29 | if agent_db_secret_id != "NOSECRET": 30 | _db_secret_string = secretsmanager_client.get_secret_value( 31 | SecretId=agent_db_secret_id 32 | )["SecretString"] 33 | _db_secret = json.loads(_db_secret_string) 34 | 35 | postgres_connection_string: str = PGVector.connection_string_from_db_params( 36 | driver="psycopg2", 37 | host=_db_secret["host"], 38 | port=_db_secret["port"], 39 | database=_db_secret["dbname"], 40 | user=_db_secret["username"], 41 | password=_db_secret["password"], 42 | ) 43 | 44 | collection_name: str = "agentic_assistant_vector_store" 45 | embedding_model_id: str = "amazon.titan-embed-text-v1" 46 | 47 | sqlalchemy_connection_url: str = sqlalchemy.URL.create( 48 | "postgresql+psycopg2", 49 | username=_db_secret["username"], 50 | password=_db_secret["password"], 51 | host=_db_secret["host"], 52 | database=_db_secret["dbname"], 53 | ) 54 | 55 | # number of sample rows to include in the prompt from the SQL table. 56 | num_sql_table_sample_rows: int = 2 57 | 58 | sql_engine = sqlalchemy.create_engine(sqlalchemy_connection_url) 59 | 60 | try: 61 | entities_db = SQLDatabase( 62 | engine=sql_engine, 63 | include_tables=SQL_TABLE_NAMES, 64 | sample_rows_in_table_info=num_sql_table_sample_rows, 65 | ) 66 | except ValueError as e: 67 | if "include_tables" in str(e): 68 | print(f"Warning: Table {SQL_TABLE_NAMES[0]} not found in the database. Proceeding without including this table.") 69 | entities_db = SQLDatabase( 70 | engine=sql_engine, 71 | include_tables=[], # Include all tables 72 | sample_rows_in_table_info=num_sql_table_sample_rows, 73 | ) 74 | else: 75 | raise e -------------------------------------------------------------------------------- /serverless_llm_assistant/lib/lambda-functions/agent-executor-lambda-container/agent-executor-lambda/assistant/prompts.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from langchain_core.prompts import ChatPromptTemplate 3 | 4 | # ============================================================================ 5 | # Claude basic chatbot prompt construction 6 | # ============================================================================ 7 | 8 | date_today = str(datetime.today().date()) 9 | 10 | system_message = f""" 11 | You are a friendly and knowledgeable AI assistant with a warm and approachable tone. 12 | Your goal is to provide helpful and accurate information to users while maintaining a conversational and engaging demeanor. 13 | 14 | When answering questions or responding to user inputs, please follow these guidelines: 15 | 16 | 1. Use the conversation history inside to provide specific details and context, but focus on summarizing or highlighting only the most recent or relevant parts to keep responses concise. 17 | 2. If you do not have enough information to provide a complete answer, acknowledge the knowledge gap politely, offer to research the topic further, and suggest authoritative sources the user could consult. 18 | 3. Adjust your language and tone to be slightly more formal or casual based on the user's communication style, but always remain professional and respectful. 19 | 4. If the conversation involves a specialized domain or topic you have particular expertise in, feel free to incorporate that knowledge to provide more insightful and in-depth responses. 20 | 5. Your response must be a valid markdown string put inside xml tags. 21 | 22 | The date today is {date_today}. 23 | """ 24 | 25 | user_message = """ 26 | Current conversation history: 27 | 28 | {history} 29 | 30 | 31 | Here is the human's next reply: 32 | 33 | {input} 34 | 35 | """ 36 | 37 | # Construct the prompt from the messages 38 | messages = [ 39 | ("system", system_message), 40 | ("human", user_message), 41 | ] 42 | 43 | CLAUDE_PROMPT = ChatPromptTemplate.from_messages(messages) 44 | 45 | ## Placeholder for lab 3 - agent prompt code 46 | ## replace this placeholder with code from lab 3, step 2 as instructed. 47 | -------------------------------------------------------------------------------- /serverless_llm_assistant/lib/lambda-functions/agent-executor-lambda-container/agent-executor-lambda/assistant/sql_chain.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Union 2 | 3 | from langchain.schema.language_model import BaseLanguageModel 4 | from langchain.schema.output_parser import NoOpOutputParser 5 | from langchain.schema.prompt_template import BasePromptTemplate 6 | from langchain.schema.runnable import RunnableMap, RunnableSequence 7 | from langchain.sql_database import SQLDatabase 8 | from langchain.chains.sql_database.query import SQLInput, SQLInputWithTables, _strip 9 | 10 | 11 | def create_sql_query_generation_chain( 12 | llm: BaseLanguageModel, 13 | db: SQLDatabase, 14 | prompt: Optional[BasePromptTemplate] = None, 15 | k: int = 5, 16 | ) -> RunnableSequence[Union[SQLInput, SQLInputWithTables], str]: 17 | """Create a chain that generates SQL queries. 18 | 19 | Args: 20 | llm: The language model to use 21 | db: The SQLDatabase to generate the query for 22 | prompt: The prompt to use. If none is provided, will choose one 23 | based on dialect. Defaults to None. 24 | k: The number of results per select statement to return. Defaults to 5. 25 | 26 | Returns: 27 | A chain that takes in a question and generates a SQL query that answers 28 | that question. 29 | """ 30 | if prompt is not None: 31 | prompt_to_use = prompt 32 | else: 33 | raise ValueError( 34 | "A valid SQL query generation prompt must be provided." 35 | f" Current prompt is {prompt}" 36 | ) 37 | 38 | inputs = { 39 | "input": lambda x: x["question"], 40 | "initial_context": lambda x: x["initial_context"], 41 | "tables_content_description": lambda x: x["tables_content_description"], 42 | "top_k": lambda _: k, 43 | "table_info": lambda x: db.get_table_info( 44 | table_names=x.get("table_names_to_use") 45 | ), 46 | } 47 | if "dialect" in prompt_to_use.input_variables: 48 | inputs["dialect"] = lambda _: db.dialect 49 | return ( 50 | RunnableMap(inputs) 51 | | prompt_to_use 52 | | llm.bind(stop=["\nSQLQuery:"]) 53 | | NoOpOutputParser() 54 | | _strip 55 | ) -------------------------------------------------------------------------------- /serverless_llm_assistant/lib/lambda-functions/agent-executor-lambda-container/agent-executor-lambda/assistant/sqlqa.py: -------------------------------------------------------------------------------- 1 | from langchain.prompts.prompt import PromptTemplate 2 | 3 | from .config import AgenticAssistantConfig 4 | from .sql_chain import create_sql_query_generation_chain 5 | 6 | config = AgenticAssistantConfig() 7 | 8 | sql_tables_content_description = { 9 | "extracted_entities": ( 10 | "Contains extracted information from multiple financial reports of companies." 11 | " The information includes revenue, number of employees and risks per company per year." 12 | ) 13 | } 14 | 15 | # ============================================================================ 16 | # Prompt construction for SQL QA. 17 | # ============================================================================ 18 | _SQL_TEMPLATE = """ 19 | \nHuman: Given the input question inside the XML tags, write a syntactically correct {dialect} query that will be executed against a SQL database. 20 | You must follow the rules within the XML tags below: 21 | 22 | 23 | 1. Only generate the SQL query without explanation. 24 | 2. You must only use column names that exist in the table schema below. 25 | 3. Only use relevant columns to the input question and pay attention to use the correct data types when filtering with WHERE. 26 | 4. Consider the information inside the XML tags as intitial context when writing the SQL query. 27 | 7. End the SQL with a LIMIT clause. The limit value is inside the XML tags below. 28 | 8. When using GROUP BY, ensure that every column in the SELECT clause appears in the GROUP BY clause or that it is aggregated with an aggregation function such as AVG or SUM. 29 | 9. Write the least complex SQL query that answers the questions and abides by the rules. 30 | 31 | 32 | Use the following format: 33 | 34 | Question: "Question here" 35 | Initial context: "Initial context here" 36 | SQLQuery: "SQL Query to run" 37 | 38 | Consider the table descriptions inside the XML tags to choose which table(s) to use: 39 | 40 | {tables_content_description} 41 | 42 | 43 | Only use the following table schema between the XML tags
: 44 | 45 | {table_info} 46 |
47 | 48 | limit: 49 | {top_k} 50 | 51 | Assistant: 52 | Question: {input} 53 | Initial Context: 54 | 55 | {initial_context} 56 | 57 | SQLQuery: """ 58 | 59 | LLM_SQL_PROMPT = PromptTemplate( 60 | input_variables=[ 61 | "input", 62 | "table_info", 63 | "top_k", 64 | "dialect", 65 | "initial_context", 66 | "tables_content_description", 67 | ], 68 | template=_SQL_TEMPLATE, 69 | ) 70 | 71 | 72 | def prepare_tables_description(table_descriptions): 73 | table_description = "" 74 | for table, description in table_descriptions.items(): 75 | table_description += "\n" + table + ": " + description 76 | table_description += "\n" 77 | return table_description 78 | 79 | 80 | def get_text_to_sql_chain(config, llm): 81 | """Create an LLM chain to convert text input to SQL queries.""" 82 | return create_sql_query_generation_chain( 83 | llm=llm, 84 | db=config.entities_db, 85 | prompt=LLM_SQL_PROMPT, 86 | # Value to use with LIMIT clause 87 | k=5, 88 | ) 89 | 90 | 91 | def get_sql_qa_tool(user_question, text_to_sql_chain, initial_context=""): 92 | sql_query = text_to_sql_chain.invoke( 93 | { 94 | "question": user_question, 95 | "initial_context": initial_context, 96 | "tables_content_description": prepare_tables_description( 97 | sql_tables_content_description 98 | ), 99 | } 100 | ) 101 | sql_query = sql_query.strip() 102 | 103 | # Typically sql queries end with a semicolon ";", some DBs such as SQLite 104 | # fail without it, therefore, we ensure it is there. 105 | if not sql_query.endswith(";"): 106 | sql_query += ";" 107 | 108 | print(sql_query) 109 | 110 | # fixed_query = sqlfluff.fix(sql=sql_query, dialect="postgres") 111 | try: 112 | result = config.entities_db.run(sql_query) 113 | except Exception as e: 114 | result = ( 115 | f"Failed to run the SQL query {sql_query} with error {e}" 116 | " Appologize, ask the user for further specifications," 117 | " or to try again later." 118 | ) 119 | 120 | return result -------------------------------------------------------------------------------- /serverless_llm_assistant/lib/lambda-functions/agent-executor-lambda-container/agent-executor-lambda/assistant/tools.py: -------------------------------------------------------------------------------- 1 | # This module will be edited in Lab 03 to add the agent tools. 2 | import boto3 3 | from langchain.agents import Tool 4 | from langchain_aws import BedrockLLM 5 | from langchain_aws import ChatBedrock 6 | 7 | from langchain_community.tools import DuckDuckGoSearchRun 8 | from .calculator import CustomCalculatorTool 9 | from .config import AgenticAssistantConfig 10 | 11 | config = AgenticAssistantConfig() 12 | bedrock_runtime = boto3.client("bedrock-runtime", region_name=config.bedrock_region) 13 | 14 | claude_llm = BedrockLLM( 15 | model_id=config.llm_model_id, 16 | client=bedrock_runtime, 17 | model_kwargs={ 18 | "max_tokens": 1000, 19 | "temperature": 0.0, 20 | "top_p": 0.99 21 | }, 22 | ) 23 | 24 | claude_chat_llm = ChatBedrock( 25 | # model_id=config.llm_model_id, 26 | # transitioning to claude 3 with messages API 27 | model_id="anthropic.claude-3-haiku-20240307-v1:0", 28 | client=bedrock_runtime, 29 | model_kwargs={ 30 | "max_tokens": 1000, 31 | "temperature": 0.0, 32 | "top_p": 0.99 33 | }, 34 | ) 35 | -------------------------------------------------------------------------------- /serverless_llm_assistant/lib/lambda-functions/agent-executor-lambda-container/agent-executor-lambda/assistant/utils.py: -------------------------------------------------------------------------------- 1 | from bs4 import BeautifulSoup 2 | 3 | 4 | def parse_markdown_content(text): 5 | """ 6 | Parses the content between and tags from the given text. 7 | 8 | Args: 9 | text (str): The input text containing the markdown tag content. 10 | 11 | Returns: 12 | str: The content between the markdown tags, or an empty string if not found. 13 | """ 14 | soup = BeautifulSoup(text, 'html.parser') 15 | markdown_tag = soup.find('markdown') 16 | 17 | if markdown_tag: 18 | return markdown_tag.get_text() 19 | else: 20 | return '' -------------------------------------------------------------------------------- /serverless_llm_assistant/lib/lambda-functions/agent-executor-lambda-container/agent-executor-lambda/handler.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import traceback 3 | 4 | import boto3 5 | from langchain.chains import ConversationChain 6 | from langchain_aws import BedrockLLM 7 | from langchain_aws import ChatBedrock 8 | from langchain.memory import ConversationBufferMemory 9 | from langchain_community.chat_message_histories import DynamoDBChatMessageHistory 10 | 11 | from assistant.config import AgenticAssistantConfig 12 | from assistant.prompts import CLAUDE_PROMPT 13 | from assistant.utils import parse_markdown_content 14 | ## placeholder for lab 3, step 4.2, replace this with imports as instructed 15 | 16 | logger = logging.getLogger() 17 | logger.setLevel(logging.INFO) 18 | 19 | ssm = boto3.client("ssm") 20 | config = AgenticAssistantConfig() 21 | 22 | bedrock_runtime = boto3.client("bedrock-runtime", region_name=config.bedrock_region) 23 | 24 | claude_llm = BedrockLLM( 25 | model_id=config.llm_model_id, 26 | client=bedrock_runtime, 27 | model_kwargs={ 28 | "max_tokens_to_sample": 1000, 29 | "temperature": 0.0, 30 | "top_p": 0.99 31 | }, 32 | ) 33 | 34 | claude_chat_llm = ChatBedrock( 35 | # model_id=config.llm_model_id, 36 | # transitioning to claude 3 with messages API 37 | model_id="anthropic.claude-3-haiku-20240307-v1:0", 38 | client=bedrock_runtime, 39 | model_kwargs={ 40 | "max_tokens": 1000, 41 | "temperature": 0.0, 42 | "top_p": 0.99 43 | }, 44 | ) 45 | 46 | 47 | def get_basic_chatbot_conversation_chain( 48 | user_input, session_id, clean_history, verbose=True 49 | ): 50 | message_history = DynamoDBChatMessageHistory( 51 | table_name=config.chat_message_history_table_name, session_id=session_id 52 | ) 53 | 54 | if clean_history: 55 | message_history.clear() 56 | 57 | memory = ConversationBufferMemory( 58 | memory_key="history", 59 | chat_memory=message_history, 60 | # Change the human_prefix from Human to something else 61 | # to not conflict with Human keyword in Anthropic Claude model. 62 | human_prefix="Hu", 63 | return_messages=False 64 | ) 65 | 66 | conversation_chain = ConversationChain( 67 | prompt=CLAUDE_PROMPT, llm=claude_chat_llm, verbose=verbose, memory=memory 68 | ) 69 | 70 | return conversation_chain 71 | 72 | 73 | ## placeholder for lab 3, step 4.3, replace this with the get_agentic_chatbot_conversation_chain helper. 74 | 75 | 76 | def lambda_handler(event, context): 77 | logger.info(event) 78 | user_input = event["user_input"] 79 | session_id = event["session_id"] 80 | chatbot_type = event.get("chatbot_type", "basic") 81 | chatbot_types = ["basic", "agentic"] 82 | clean_history = event.get("clean_history", False) 83 | 84 | if chatbot_type == "basic": 85 | conversation_chain = get_basic_chatbot_conversation_chain( 86 | user_input, session_id, clean_history 87 | ).predict 88 | elif chatbot_type == "agentic": 89 | return { 90 | "statusCode": 200, 91 | "response": ( 92 | f"The agentic mode is not supported yet. Extend the code as instructed" 93 | " in lab 3 to add it." 94 | ), 95 | } 96 | else: 97 | return { 98 | "statusCode": 200, 99 | "response": ( 100 | f"The chatbot_type {chatbot_type} is not supported." 101 | f" Please use one of the following types: {chatbot_types}" 102 | ), 103 | } 104 | 105 | try: 106 | response = conversation_chain(input=user_input) 107 | response = parse_markdown_content(response) 108 | except Exception: 109 | response = ( 110 | "Unable to respond due to an internal issue." " Please try again later" 111 | ) 112 | print(traceback.format_exc()) 113 | 114 | return {"statusCode": 200, "response": response} 115 | -------------------------------------------------------------------------------- /serverless_llm_assistant/lib/lambda-functions/agent-executor-lambda-container/requirements.txt: -------------------------------------------------------------------------------- 1 | # Sepcify boto3 and botocore versions with Amazon Bedrock access. 2 | boto3>=1.28.57 3 | botocore>=1.31.57 4 | langchain==0.2.9 5 | langchain-aws==0.1.11 6 | langchain-community==0.2.7 7 | duckduckgo_search[lxml]==5.3.1b1 8 | langchain_postgres==0.0.9 9 | psycopg2-binary==2.9.9 10 | pgvector==0.2.5 11 | beautifulsoup4==4.12.3 12 | sqlalchemy==2.0.30 -------------------------------------------------------------------------------- /serverless_llm_assistant/lib/serverless_llm_assistant-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | import * as dynamodb from "aws-cdk-lib/aws-dynamodb"; 4 | import * as ec2 from "aws-cdk-lib/aws-ec2"; 5 | import * as iam from "aws-cdk-lib/aws-iam"; 6 | import * as lambda from "aws-cdk-lib/aws-lambda"; 7 | import * as path from "path"; 8 | import * as rds from "aws-cdk-lib/aws-rds"; 9 | import * as ssm from "aws-cdk-lib/aws-ssm"; 10 | import * as s3 from "aws-cdk-lib/aws-s3"; 11 | import { NetworkMode } from "aws-cdk-lib/aws-ecr-assets"; 12 | 13 | import { Vpc } from "./assistant-vpc"; 14 | import { AssistantApiConstruct } from "./assistant-api-gateway"; 15 | import { CognitoConstruct } from "./assistant-authorizer"; 16 | import { SageMakerRdsAccessConstruct } from "./assistant-sagemaker-postgres-acess"; 17 | import { SageMakerIAMPolicyConstruct } from "./assistant-sagemaker-iam-policy"; 18 | import { SageMakerProcessor } from "./assistant-sagemaker-processor"; 19 | 20 | const AGENT_DB_NAME = "AgentSQLDBandVectorStore"; 21 | 22 | export class ServerlessLlmAssistantStack extends cdk.Stack { 23 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 24 | super(scope, id, props); 25 | 26 | // ----------------------------------------------------------------------- 27 | // VPC Construct 28 | // Create subnets and VPC endpoints 29 | // const vpc = new Vpc(this, "Vpc"); 30 | 31 | // ----------------------------------------------------------------------- 32 | // Create relevant SSM parameters 33 | const parameters = this.node.tryGetContext("parameters") || { 34 | bedrock_region: "us-west-2", 35 | llm_model_id: "anthropic.claude-v2", 36 | }; 37 | 38 | const BEDROCK_REGION = parameters["bedrock_region"]; 39 | const LLM_MODEL_ID = parameters["llm_model_id"]; 40 | 41 | // Note: the SSM parameters for Bedrock region and endpoint are used 42 | // to setup a boto3 bedrock client for programmatic access to Bedrock APIs. 43 | 44 | // Add an SSM parameter for the Bedrock region. 45 | const ssm_bedrock_region_parameter = new ssm.StringParameter( 46 | this, 47 | "ssmBedrockRegionParameter", 48 | { 49 | parameterName: "/AgenticLLMAssistantWorkshop/bedrock_region", 50 | // This is the default region. 51 | // The user can update it in parameter store. 52 | stringValue: BEDROCK_REGION, 53 | } 54 | ); 55 | 56 | // Add an SSM parameter for the llm model id. 57 | const ssm_llm_model_id_parameter = new ssm.StringParameter( 58 | this, 59 | "ssmLLMModelIDParameter", 60 | { 61 | parameterName: "/AgenticLLMAssistantWorkshop/llm_model_id", 62 | // This is the default region. 63 | // The user can update it in parameter store. 64 | stringValue: LLM_MODEL_ID, 65 | } 66 | ); 67 | 68 | // ----------------------------------------------------------------------- 69 | // Placeholder for Lab 4, step 2.2 - Put the database resource definition here. 70 | 71 | // ----------------------------------------------------------------------- 72 | // Lab 4. Step 4.1 - configure sagemaker access to the database. 73 | // Create a security group to allow access to the DB from a SageMaker processing job 74 | // which will be used to index embedding vectors. 75 | 76 | // const sagemaker_rds_access = new SageMakerRdsAccessConstruct(this, 'SageMakerRdsAccess', { 77 | // vpc: vpc.vpc, 78 | // rdsCluster: AgentDB, 79 | // }); 80 | 81 | // ----------------------------------------------------------------------- 82 | // Add a DynamoDB table to store chat history per session id. 83 | 84 | // When you see a need for it, consider configuring autoscaling to the table 85 | // https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_dynamodb-readme.html#configure-autoscaling-for-your-table 86 | const ChatMessageHistoryTable = new dynamodb.Table( 87 | this, 88 | "ChatHistoryTable", 89 | { 90 | // consider activating the encryption by uncommenting the code below. 91 | // encryption: dynamodb.TableEncryption.AWS_MANAGED, 92 | partitionKey: { 93 | name: "SessionId", 94 | type: dynamodb.AttributeType.STRING, 95 | }, 96 | billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, 97 | // Considerations when choosing a table class 98 | // https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/WorkingWithTables.tableclasses.html 99 | tableClass: dynamodb.TableClass.STANDARD, 100 | // When moving to production, use cdk.RemovalPolicy.RETAIN instead 101 | // which will keep the database table when destroying the stack. 102 | // this avoids accidental deletion of user data. 103 | removalPolicy: cdk.RemovalPolicy.DESTROY, 104 | encryption: dynamodb.TableEncryption.AWS_MANAGED, 105 | } 106 | ); 107 | 108 | // ----------------------------------------------------------------------- 109 | var currentNetworkMode = NetworkMode.DEFAULT; 110 | // if you run the cdk stack in SageMaker editor, you need to pass --network sagemaker 111 | // for docker build to work. The following achieve that. 112 | if (process.env.SAGEMAKER_APP_TYPE) { 113 | currentNetworkMode = NetworkMode.custom("sagemaker"); 114 | } 115 | 116 | // Add AWS Lambda container and function to serve as the agent executor. 117 | const agent_executor_lambda = new lambda.DockerImageFunction( 118 | this, 119 | "LambdaAgentContainer", 120 | { 121 | code: lambda.DockerImageCode.fromImageAsset( 122 | path.join( 123 | __dirname, 124 | "lambda-functions/agent-executor-lambda-container" 125 | ), 126 | { 127 | networkMode: currentNetworkMode, 128 | buildArgs: { "--platform": "linux/amd64" }, 129 | } 130 | ), 131 | description: "Lambda function with bedrock access created via CDK", 132 | timeout: cdk.Duration.minutes(5), 133 | memorySize: 2048, 134 | // vpc: vpc.vpc, 135 | environment: { 136 | BEDROCK_REGION_PARAMETER: ssm_bedrock_region_parameter.parameterName, 137 | LLM_MODEL_ID_PARAMETER: ssm_llm_model_id_parameter.parameterName, 138 | CHAT_MESSAGE_HISTORY_TABLE: ChatMessageHistoryTable.tableName, 139 | // AGENT_DB_SECRET_ID: AgentDB.secret?.secretArn as string 140 | }, 141 | } 142 | ); 143 | 144 | // Placeholder Step 2.4 - grant Lambda permission to access db credentials 145 | 146 | // Allow Lambda to read SSM parameters. 147 | ssm_bedrock_region_parameter.grantRead(agent_executor_lambda); 148 | ssm_llm_model_id_parameter.grantRead(agent_executor_lambda); 149 | 150 | // Allow Lambda read/write access to the chat history DynamoDB table 151 | // to be able to read and update it as conversations progress. 152 | ChatMessageHistoryTable.grantReadWriteData(agent_executor_lambda); 153 | 154 | // Allow the Lambda function to use Bedrock 155 | agent_executor_lambda.role?.addManagedPolicy( 156 | iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonBedrockFullAccess") 157 | ); 158 | 159 | // Save the Lambda ARN in an SSM parameter to simplify invoking the lambda 160 | // from a SageMaker notebook, without having to copy it manually. 161 | const agentLambdaNameParameter = new ssm.StringParameter( 162 | this, 163 | "AgentLambdaNameParameter", 164 | { 165 | parameterName: 166 | "/AgenticLLMAssistantWorkshop/AgentExecutorLambdaNameParameter", 167 | stringValue: agent_executor_lambda.functionName, 168 | } 169 | ); 170 | 171 | //------------------------------------------------------------------------ 172 | // Create an S3 bucket for intermediate data staging 173 | // and allow SageMaker to read and write to it. 174 | const agent_data_bucket = new s3.Bucket(this, "AgentDataBucket", { 175 | // Warning, swith DESTROY to RETAIN to avoid accidental deletion 176 | // of important data. 177 | removalPolicy: cdk.RemovalPolicy.DESTROY, 178 | autoDeleteObjects: true, 179 | }); 180 | 181 | // Save the bucket name as an SSM parameter to simplify using it in 182 | // SageMaker processing jobs without having to copy the name manually. 183 | const agentDataBucketParameter = new ssm.StringParameter( 184 | this, 185 | "AgentDataBucketParameter", 186 | { 187 | parameterName: "/AgenticLLMAssistantWorkshop/AgentDataBucketParameter", 188 | stringValue: agent_data_bucket.bucketName, 189 | } 190 | ); 191 | 192 | // ----------------------------------------------------------------------- 193 | // Create a managed IAM policy to be attached to a SageMaker execution role 194 | // to allow the required permissions to retrieve the information to access the database. 195 | // new SageMakerIAMPolicyConstruct(this, 'SageMakerIAMPolicy', { 196 | // bedrockRegionParameter: ssm_bedrock_region_parameter, 197 | // llmModelIdParameter: ssm_llm_model_id_parameter, 198 | // agentDataBucketParameter: agentDataBucketParameter, 199 | // agentLambdaNameParameter: agentLambdaNameParameter, 200 | // agentDataBucket: agent_data_bucket, 201 | // agentExecutorLambda: agent_executor_lambda, 202 | // rdsCluster: AgentDB, 203 | // sagemaker_rds_access: sagemaker_rds_access, 204 | // }); 205 | 206 | // ----------------------------------------------------------------------- 207 | // Create a new Cognito user pool and add an app client to the user pool 208 | 209 | const cognito_authorizer = new CognitoConstruct(this, "Cognito"); 210 | // ------------------------------------------------------------------------- 211 | // Add an Amazon API Gateway with AWS cognito auth and an AWS lambda as a backend 212 | new AssistantApiConstruct(this, "AgentApi", { 213 | cognitoUserPool: cognito_authorizer.userPool, 214 | lambdaFunction: agent_executor_lambda, 215 | }); 216 | 217 | new SageMakerProcessor(this, "SagemakerProcessor"); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /serverless_llm_assistant/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless_llm_assistant", 3 | "version": "0.1.0", 4 | "bin": { 5 | "serverless_llm_assistant": "bin/serverless_llm_assistant.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@types/jest": "^29.5.11", 15 | "@types/node": "20.11.14", 16 | "jest": "^29.7.0", 17 | "ts-jest": "^29.1.2", 18 | "aws-cdk": "2.126.0", 19 | "ts-node": "^10.9.2", 20 | "typescript": "~5.3.3" 21 | }, 22 | "dependencies": { 23 | "aws-cdk-lib": "2.126.0", 24 | "constructs": "^10.0.0", 25 | "source-map-support": "^0.5.21" 26 | } 27 | } -------------------------------------------------------------------------------- /serverless_llm_assistant/test/serverless_llm_assistant.test.ts: -------------------------------------------------------------------------------- 1 | // import * as cdk from 'aws-cdk-lib'; 2 | // import { Template } from 'aws-cdk-lib/assertions'; 3 | // import * as ServerlessLlmAssistant from '../lib/serverless_llm_assistant-stack'; 4 | 5 | // example test. To run these tests, uncomment this file along with the 6 | // example resource in lib/serverless_llm_assistant-stack.ts 7 | test('SQS Queue Created', () => { 8 | // const app = new cdk.App(); 9 | // // WHEN 10 | // const stack = new ServerlessLlmAssistant.ServerlessLlmAssistantStack(app, 'MyTestStack'); 11 | // // THEN 12 | // const template = Template.fromStack(stack); 13 | 14 | // template.hasResourceProperties('AWS::SQS::Queue', { 15 | // VisibilityTimeout: 300 16 | // }); 17 | }); 18 | -------------------------------------------------------------------------------- /serverless_llm_assistant/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2020", 7 | "dom" 8 | ], 9 | "declaration": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": false, 19 | "inlineSourceMap": true, 20 | "inlineSources": true, 21 | "experimentalDecorators": true, 22 | "strictPropertyInitialization": false, 23 | "typeRoots": [ 24 | "./node_modules/@types" 25 | ] 26 | }, 27 | "exclude": [ 28 | "node_modules", 29 | "cdk.out" 30 | ] 31 | } 32 | --------------------------------------------------------------------------------