├── lab2 ├── infra │ ├── __init__.py │ ├── constructs │ │ ├── __init__.py │ │ ├── bdrk_reinvent_layers.py │ │ └── bdrk_reinvent_api.py │ ├── stacks │ │ ├── __init__.py │ │ └── bdrk_reinvent_streamlit.py │ └── bdrk_reinvent_stack.py ├── assets │ ├── streamlit │ │ ├── src │ │ │ ├── components │ │ │ │ ├── __init__.py │ │ │ │ ├── sm_endpoints_model_specs.json │ │ │ │ ├── bedrock_model_specs.json │ │ │ │ ├── sns_api.py │ │ │ │ ├── utils_models.py │ │ │ │ ├── genai_api.py │ │ │ │ ├── utils.py │ │ │ │ └── authenticate.py │ │ │ ├── assets │ │ │ │ └── AWS_logo_RGB.png │ │ │ ├── data │ │ │ │ ├── customer_variables.txt │ │ │ │ ├── df_item_banking.csv │ │ │ │ ├── df_segment_data.csv │ │ │ │ └── products.json │ │ │ ├── app_pages │ │ │ │ ├── CustomerList.py │ │ │ │ ├── PromptCatalog.py │ │ │ │ └── BuildPromptTemplate.py │ │ │ └── Home.py │ │ ├── .streamlit │ │ │ ├── config.toml │ │ │ └── pages.toml │ │ ├── build_docker.sh │ │ ├── pyproject.toml │ │ ├── Dockerfile │ │ └── .env │ ├── layers │ │ └── dummy.txt │ └── lambda │ │ ├── genai │ │ └── bedrock_content_generation_lambda │ │ │ ├── model_configs │ │ │ ├── amazon.titan-text-express-v1.json │ │ │ ├── anthropic.claude-v2.json │ │ │ └── anthropic.claude-3-sonnet-20240229-v1.json │ │ │ └── bedrock_content_generation_lambda.py │ │ ├── sns_topic_lambda │ │ └── sns_topic_lambda.py │ │ └── db_connections │ │ └── prompt_lambda │ │ └── prompt_lambda.py ├── .gitignore ├── .pre-commit-config.yaml ├── cdk.json ├── app.py ├── README.md ├── config.yml ├── pyproject.toml └── run.sh ├── .gitignore ├── CODE_OF_CONDUCT.md ├── SUPPORT.MD ├── lab1 ├── sample-questions.txt ├── bank-call-centre-transcript.txt └── ML-blog.txt ├── SECURITY.MD ├── CHANGELOG.MD ├── LICENSE ├── PULL_REQUEST_TEMPLATE.MD ├── README.md ├── ISSUE_TEMPLATE.MD └── CONTRIBUTING.md /lab2/infra/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /lab2/infra/constructs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lab2/infra/stacks/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lab2/assets/streamlit/src/components/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lab2/assets/layers/dummy.txt: -------------------------------------------------------------------------------- 1 | Just a placeholder file in the directory to be created by the installer. Otherwise git would ignore the directory. -------------------------------------------------------------------------------- /lab2/assets/lambda/genai/bedrock_content_generation_lambda/model_configs/amazon.titan-text-express-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "EXAMPLES": null, 3 | "STOP_WORDS": [], 4 | "TOP_P": 0.9 5 | } 6 | -------------------------------------------------------------------------------- /lab2/assets/lambda/genai/bedrock_content_generation_lambda/model_configs/anthropic.claude-v2.json: -------------------------------------------------------------------------------- 1 | { 2 | "EXAMPLES": null, 3 | "STOP_WORDS": ["\n\nHuman:"], 4 | "TOP_P": 0.9 5 | } 6 | -------------------------------------------------------------------------------- /lab2/assets/lambda/genai/bedrock_content_generation_lambda/model_configs/anthropic.claude-3-sonnet-20240229-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "EXAMPLES": null, 3 | "STOP_WORDS": ["\n\nHuman:"], 4 | "TOP_P": 0.9 5 | } 6 | -------------------------------------------------------------------------------- /lab2/assets/streamlit/src/assets/AWS_logo_RGB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/build-scale-generative-ai-applications-with-amazon-bedrock-workshop/HEAD/lab2/assets/streamlit/src/assets/AWS_logo_RGB.png -------------------------------------------------------------------------------- /lab2/assets/streamlit/.streamlit/config.toml: -------------------------------------------------------------------------------- 1 | [server] 2 | enableCORS=false 3 | enableWebsocketCompression=false 4 | enableXsrfProtection=false 5 | headless=true 6 | 7 | [browser] 8 | gatherUsageStats = false 9 | 10 | [theme] 11 | base="light" -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /SUPPORT.MD: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | ## How to file issues and get help 4 | 5 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 6 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 7 | feature request as a new Issue. 8 | 9 | For help and questions about using this project, please reach out to repository maintainers by raising an issue. 10 | 11 | -------------------------------------------------------------------------------- /lab2/.gitignore: -------------------------------------------------------------------------------- 1 | assets/streamlit/src/.env 2 | auxilary-assets/rsvps_GenAI-Prompting-Hackathon-EMEA_2023-10-06T12%3A03%3A45.352Z.xlsx 3 | auxilary-assets/RSVPS.csv 4 | auxilary-assets/RSVPS.xlsx 5 | .venv/ 6 | **/cdk.out/ 7 | **/.DS_Store 8 | **/__pycache__/ 9 | cdk.context.json 10 | infra/constructs/.DS_Store 11 | infra/constructs/.DS_Store 12 | infra/stacks/.DS_Store 13 | requirements.txt 14 | infra/constructs/.DS_Store 15 | assets/layers/bedrock-compatible-sdk.zip -------------------------------------------------------------------------------- /lab2/assets/streamlit/build_docker.sh: -------------------------------------------------------------------------------- 1 | # Configure Account and region 2 | 3 | AWS_ACCOUNT_ID=$1 4 | AWS_REGION='eu-west-1' 5 | 6 | IMAGE_TAG='demo-streamlit' 7 | ECR_REPOSITORY='demo-streamlit' 8 | 9 | docker build . --tag $IMAGE_TAG 10 | docker tag $IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$ECR_REPOSITORY:latest 11 | eval $(aws ecr get-login --no-include-email) 12 | docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$ECR_REPOSITORY:latest -------------------------------------------------------------------------------- /lab1/sample-questions.txt: -------------------------------------------------------------------------------- 1 | Can you write a summary of the above conversation? 2 | 3 | What troubleshooting steps were suggested to the customer to fix their issue? 4 | 5 | Was the customer able to login after they tried resetting the password by themselves? 6 | 7 | What is the overall sentiment and sentiment score of the conversation between the customer and the agent? 8 | 9 | Identify any specific words, phrases, or context that influenced the positive sentiment 10 | 11 | -------------------------------------------------------------------------------- /lab2/assets/streamlit/.streamlit/pages.toml: -------------------------------------------------------------------------------- 1 | [[pages]] 2 | path="src/Home.py" 3 | name="Home" 4 | icon="🏠" 5 | 6 | [[pages]] 7 | path="src/app_pages/CustomerList.py" 8 | name="Customer List" 9 | icon="📖" 10 | 11 | [[pages]] 12 | path="src/app_pages/BuildPromptTemplate.py" 13 | name="Build Prompt Template" 14 | icon="🪄" 15 | 16 | [[pages]] 17 | path="src/app_pages/PromptCatalog.py" 18 | name="Prompt Catalog" 19 | icon="📖" 20 | 21 | [[pages]] 22 | path="src/app_pages/EmailGenerationWizard.py" 23 | name="Email Generation Wizard" 24 | icon="🧙🏻‍♀️" -------------------------------------------------------------------------------- /lab2/.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pycqa/isort 3 | rev: 5.12.0 4 | hooks: 5 | - id: isort 6 | name: isort (python) 7 | args: ["--profile", "black"] 8 | 9 | - repo: https://github.com/psf/black 10 | rev: 23.3.0 # Use the ref you want to point at 11 | hooks: 12 | - id: black 13 | args: ["--line-length", "120"] 14 | 15 | - repo: https://github.com/astral-sh/ruff-pre-commit 16 | rev: v0.0.272 # Ruff version. 17 | hooks: 18 | - id: ruff 19 | args: ["--fix", "--exit-non-zero-on-fix"] -------------------------------------------------------------------------------- /SECURITY.MD: -------------------------------------------------------------------------------- 1 | - [Security Policy](#security-policy) 2 | - [Supported Versions](#supported-versions) 3 | - [Reporting a Vulnerability](#reporting-a-vulnerability) 4 | 5 | 6 | # Security Policy 7 | 8 | ## Supported Versions 9 | 10 | We release patches for security vulnerabilities. Which versions are eligible for 11 | receiving such patches depends on the CVSS v3.0 Rating: 12 | 13 | 14 | ## Reporting a Vulnerability 15 | 16 | Please report (suspected) security vulnerabilities by: 17 | **Raising an issue in the repository within 48 hours. If the issue is confirmed, we will release a patch as soon 18 | as possible depending on complexity but historically within a few days. -------------------------------------------------------------------------------- /CHANGELOG.MD: -------------------------------------------------------------------------------- 1 | 2 | # Change Log 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | ## [Unreleased] - yyyy-mm-dd 9 | 10 | Here we write upgrading notes for brands. It's a team effort to make them as 11 | straightforward as possible. 12 | 13 | ### Added 14 | - [PROJECTNAME-XXXX](http://tickets.projectname.com/browse/PROJECTNAME-XXXX) 15 | MINOR Ticket title goes here. 16 | - [PROJECTNAME-YYYY](http://tickets.projectname.com/browse/PROJECTNAME-YYYY) 17 | PATCH Ticket title goes here. 18 | 19 | ### Changed 20 | 21 | ### Fixed 22 | 23 | ## [1.0.0] - 2023-11-21 24 | 25 | Here we would have the update steps for 1.0.0 for people to follow. 26 | 27 | ### Added 28 | 29 | ### Changed 30 | 31 | ### Fixed 32 | -------------------------------------------------------------------------------- /lab2/assets/streamlit/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["poetry-core>=1.7.0"] 3 | build-backend = "poetry.core.masonry.api" 4 | 5 | [tool.poetry] 6 | name = "reinvent-bedrock-workshop" 7 | version = "0.2.0" 8 | description = "Bedrock & Build Prompt Template workshop for re:Invent-23" 9 | authors = [ 10 | "Philipp Kaindl ", 11 | "Akarsha Sehwag ", 12 | "Olivier Boder ", 13 | "Vikesh Pandey ", 14 | "Tingyi Li ", 15 | ] 16 | license = "Amazon Software License" 17 | 18 | [tool.poetry.dependencies] 19 | python = ">3.9.7,<3.10.0" 20 | Requests = "^2.31.0" 21 | boto3 = "~1.27.0" 22 | streamlit = "1.35.0" 23 | st-pages = "~0.4.3" 24 | streamlit-extras = "~0.2.7" 25 | qrcode = "^7.4.2" 26 | rel = "^0.4.9" 27 | python-dotenv = "~1.0.0" 28 | pyjwt = "~2.7.0" 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /lab2/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "python3.9 app.py", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "requirements*.txt", 11 | "source.bat", 12 | "**/__init__.py", 13 | "python/__pycache__", 14 | "tests" 15 | ] 16 | }, 17 | "context": { 18 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 19 | "@aws-cdk/core:stackRelativeExports": true, 20 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 21 | "@aws-cdk/aws-lambda:recognizeVersionProps": true, 22 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true, 23 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 24 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 25 | "@aws-cdk/core:target-partitions": [ 26 | "aws", 27 | "aws-cn" 28 | ] 29 | } 30 | } -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.MD: -------------------------------------------------------------------------------- 1 | We accept pull-requests for Bug Fixes and new features **ONLY**. 2 | 3 | 4 | 5 | ## Description 6 | 7 | 8 | ## Related Issue 9 | 10 | 11 | 12 | 13 | 14 | ## Motivation and Context 15 | 16 | 17 | 18 | ## How Has This Been Tested? 19 | 20 | 21 | 22 | 23 | ## Screenshots (if appropriate): 24 | -------------------------------------------------------------------------------- /lab2/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import aws_cdk as cdk 3 | from pathlib import Path 4 | 5 | # from cdk_nag import AwsSolutionsChecks, NagSuppressions 6 | 7 | from infra.bdrk_reinvent_stack import bdrkReinventStack 8 | 9 | import yaml 10 | from yaml.loader import SafeLoader 11 | 12 | 13 | with open(os.path.join(Path(__file__).parent, "config.yml"), "r") as ymlfile: 14 | stack_config = yaml.load(ymlfile, Loader=SafeLoader) 15 | 16 | app = cdk.App() 17 | env = cdk.Environment(account=os.getenv("CDK_DEFAULT_ACCOUNT"), region=os.getenv("CDK_DEFAULT_REGION")) 18 | 19 | # cdk.Aspects.of(app).add(AwsSolutionsChecks(verbose=True)) 20 | 21 | # NagSuppressions.add_resource_suppressions( 22 | # app, 23 | # [ 24 | # { 25 | # "id": "AwsSolutions-IAM5", 26 | # "reason": "IAM permissions have been scoped by least principle but still need wildcards.", 27 | # }, 28 | # ], 29 | # ) 30 | 31 | 32 | stack = bdrkReinventStack(scope=app, stack_name=stack_config["stack_name"], config=stack_config, env=env) 33 | app.synth() 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Build and scale Generative AI applications with Amazon Bedrock - Workshop 2 | 3 | This project contains the source code for running the [**Build and scale Generative AI applications with Amazon Bedrock - Workshop**](https://catalog.us-east-1.prod.workshops.aws/workshops/e820beb4-e87e-4a85-bc5b-01548ceba1f8/en-US). It has two labs: 4 | 5 | 1. Lab 1: This folder provides supporting files need to execute lab1 of the workshop. 6 | 2. Lab 2: This folder provides the complete code for building a generative ai application and also the code for deploying the application to an AWS account. 7 | 8 | ## Pre-requisites 9 | 1. Check the **Pre-requisites** section of the [Workshop](https://catalog.us-east-1.prod.workshops.aws/workshops/e820beb4-e87e-4a85-bc5b-01548ceba1f8/en-US/pre-requisites) 10 | 11 | ## Contributions 12 | Please checkout [CONTRIBUTING.MD](CONTRIBUTING.MD). 13 | 14 | ## LICENSE 15 | This project is licensed under MIT-0. For details, See [LICENSE](LICENSE). 16 | 17 | ## Issues 18 | Please use the [ISSUE_TEMPLATE](ISSUE_TEMPLATE.MD) to raise any issues for this repository. 19 | -------------------------------------------------------------------------------- /lab2/assets/streamlit/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # This is AWS Content subject to the terms of the Customer Agreement 4 | # ---------------------------------------------------------------------- 5 | # File content: 6 | # Docker image of the streamlit container 7 | FROM --platform=linux/amd64 python:3.9-slim 8 | WORKDIR /app 9 | 10 | RUN apt-get update -y && apt-get install -y --no-install-recommends\ 11 | build-essential \ 12 | curl \ 13 | software-properties-common \ 14 | git \ 15 | && rm -rf /var/lib/apt/lists/* 16 | 17 | COPY pyproject.toml /app 18 | COPY .streamlit/ /app/.streamlit/ 19 | 20 | RUN pip3 --no-cache-dir install -U pip 21 | RUN pip3 --no-cache-dir install poetry 22 | RUN poetry config virtualenvs.create false 23 | RUN poetry install --only main 24 | 25 | COPY src/ /app/src 26 | 27 | EXPOSE 8501 28 | 29 | HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health 30 | 31 | ENTRYPOINT ["streamlit", "run", "src/Home.py", "--server.port=8501", "--server.address=0.0.0.0"] 32 | 33 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.MD: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Expected Behavior 4 | 5 | 6 | ## Current Behavior 7 | 8 | 9 | ## Possible Solution 10 | 11 | 12 | ## Steps to Reproduce 13 | 14 | 15 | 1. 16 | 2. 17 | 3. 18 | 4. 19 | 20 | ## Context (Environment) 21 | 22 | 23 | 24 | 25 | 26 | ## Detailed Description 27 | 28 | 29 | ## Possible Implementation 30 | 31 | -------------------------------------------------------------------------------- /lab2/assets/streamlit/.env: -------------------------------------------------------------------------------- 1 | # Environment variables that wil be imported by the Streamlit app when doing local testing 2 | # Paste AWS region, S3 bucket name, Cognito client ID & API URLs from the CDK deployment 3 | # Optionally, provide URLs for images used for cover images and chatbot avatar 4 | # Finally, paste temporary AWS key, secret and token from (Isengard) console (or export them directly in the terminal) 5 | 6 | REGION = "us-west-2" 7 | 8 | CLIENT_ID = "3q910cl6oae1fuqmc9ud3612mu" 9 | 10 | API_URI = "https://3ndy41dxki.execute-api.us-east-1.amazonaws.com" 11 | 12 | COVER_IMAGE_URL = "https://reinvent.awsevents.com/content/dam/reinvent/2023/media/ripples/countdown-keyart.png"#"https://d1.awsstatic.com/AWS-ImgHeader_Amazon-Kendra%20(1).b1032a8675c305031dcbde588933d212ee021ac5.png" # custom cover image on app pages 13 | COVER_IMAGE_LOGIN_URL = https://reinvent.awsevents.com/content/dam/reinvent/2023/media/ripples/countdown-keyart.png#"https://d1.awsstatic.com/AWS-ImgHeader_Amazon-Kendra%20(1).b1032a8675c305031dcbde588933d212ee021ac5.png" # default cover image on login page 14 | ASSISTANT_AVATAR_URL = "https://d1.awsstatic.com/products/bedrock/icon_64_amazonbedrock.302e08f0c3cd2a11d37eb3d77cb894bc5ceff8e4.png" 15 | 16 | -------------------------------------------------------------------------------- /lab2/assets/streamlit/src/components/sm_endpoints_model_specs.json: -------------------------------------------------------------------------------- 1 | { 2 | "falcon-7b-instruct-bf16": { 3 | "MODEL_ID": "falcon-7b-instruct-bf16", 4 | "NUM_DOCS_DEFAULT": 3, 5 | "TEMPERATURE_DEFAULT": 0.1, 6 | "ANSWER_LENGTH_DEFAULT": 200, 7 | "DOC_LENGTH_DEFAULT": 1000, 8 | "RELEVANCE_THRESHOLD_DEFAULT": 0.2 9 | }, 10 | "falcon-40b-instruct-bf16": { 11 | "MODEL_ID": "falcon-40b-instruct-bf16", 12 | "NUM_DOCS_DEFAULT": 3, 13 | "TEMPERATURE_DEFAULT": 0.1, 14 | "ANSWER_LENGTH_DEFAULT": 200, 15 | "DOC_LENGTH_DEFAULT": 1000, 16 | "RELEVANCE_THRESHOLD_DEFAULT": 0.2 17 | }, 18 | "meta-textgeneration-llama-2-7b-f": { 19 | "MODEL_ID": "meta-textgeneration-llama-2-7b-f", 20 | "NUM_DOCS_DEFAULT": 3, 21 | "TEMPERATURE_DEFAULT": 0.1, 22 | "ANSWER_LENGTH_DEFAULT": 200, 23 | "DOC_LENGTH_DEFAULT": 1000, 24 | "RELEVANCE_THRESHOLD_DEFAULT": 0.2 25 | }, 26 | "meta-textgeneration-llama-2-13b-f": { 27 | "MODEL_ID": "meta-textgeneration-llama-2-13b-f", 28 | "NUM_DOCS_DEFAULT": 3, 29 | "TEMPERATURE_DEFAULT": 0.1, 30 | "ANSWER_LENGTH_DEFAULT": 200, 31 | "DOC_LENGTH_DEFAULT": 1000, 32 | "RELEVANCE_THRESHOLD_DEFAULT": 0.2 33 | } 34 | } -------------------------------------------------------------------------------- /lab2/infra/constructs/bdrk_reinvent_layers.py: -------------------------------------------------------------------------------- 1 | """ 2 | bdrk_reinvent API constructs 3 | """ 4 | 5 | from aws_cdk import aws_lambda as _lambda 6 | from constructs import Construct 7 | 8 | 9 | class bdrk_reinventLambdaLayers(Construct): 10 | def __init__( 11 | self, 12 | scope: Construct, 13 | construct_id: str, 14 | stack_name, 15 | **kwargs, 16 | ) -> None: 17 | super().__init__(scope, construct_id, **kwargs) 18 | 19 | self.bedrock_compatible_sdk = _lambda.LayerVersion( 20 | self, 21 | f"{stack_name}-bedrock-compatible-sdk-layer", 22 | compatible_runtimes=[_lambda.Runtime.PYTHON_3_9], 23 | code=_lambda.Code.from_asset("./assets/layers/bedrock-compatible-sdk.zip"), 24 | description="A layer for bedrock compatible boto3 sdk", 25 | layer_version_name=f"{stack_name}-bedrock-compatible-sdk-layer-3", 26 | ) 27 | 28 | # self.jwt = _lambda.LayerVersion( 29 | # self, 30 | # f"{stack_name}-jwt", 31 | # compatible_runtimes=[_lambda.Runtime.PYTHON_3_9], 32 | # code=_lambda.Code.from_asset("./assets/layers/jwt.zip"), 33 | # description="A layer with jwt to decode tokens", 34 | # layer_version_name=f"{stack_name}-jwt", 35 | # ) 36 | -------------------------------------------------------------------------------- /lab2/README.md: -------------------------------------------------------------------------------- 1 | # Lab 2 2 | 3 | ## Prerequisites 4 | You need to add model access to Anthropic Claude (V2) and Amazon Titan (if possible). See the documentation for more: https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html#add-model-access 5 | 6 | ## Installation 7 | 8 | 1. Locate the `run.sh` file under Lab2 directory and make it executable: 9 | `chmod +x run.sh` 10 | 11 | 2. Execute `run.sh` 12 | `./run.sh` 13 | 14 | The deployment of the application will take approximately 10 minutes. Once done it will show outputs like: 15 | 16 | ```` 17 | bdrkReinventStack.AlbDnsName = internal-bdrkReinventStack-stl-priv-********.us-west-2.elb.amazonaws.com 18 | bdrkReinventStack.BucketName = bdrkreinventstack-data-046676399357 19 | bdrkReinventStack.bdrkReinventStackAPIAPIEndpoint4FB601DD = https://ox3d8uym1g.execute-api.us-west-2.amazonaws.com 20 | bdrkReinventStack.bdrkReinventStackAPICognitoClientID84CF997C = 2glsolf5tsd1j******** 21 | bdrkReinventStack.bdrkReinventStackAPIWSAPIEndpoint38A1018F = wss://kode5g2jke.execute-api.us-west-2.amazonaws.com/Prod 22 | bdrkReinventStack.cloudfrontdistributiondomainname = d36kltsvayb4ge.cloudfront.net 23 | Stack ARN: 24 | arn:aws:cloudformation:us-west-2:046676*****:stack/bdrkReinventStack/cad08e40-773d-11ee-a2e9-0e52***** 25 | 26 | ✨ Total time: 467.83s 27 | ``` 28 | 29 | You can access the workshop using the CloudFront URL output of the CDK stack: `bdrkReinventStack.cloudfrontdistributiondomainname` 30 | 31 | 32 | -------------------------------------------------------------------------------- /lab2/assets/streamlit/src/components/bedrock_model_specs.json: -------------------------------------------------------------------------------- 1 | { 2 | "Bedrock: Amazon Titan": { 3 | "MODEL_ID": "amazon.titan-tg1-large", 4 | "NUM_DOCS_DEFAULT": 5, 5 | "TEMPERATURE_DEFAULT": 0.0, 6 | "ANSWER_LENGTH_DEFAULT": 200, 7 | "DOC_LENGTH_DEFAULT": 1000, 8 | "RELEVANCE_THRESHOLD_DEFAULT": 0.2 9 | }, 10 | "Bedrock: Claude 3 Sonnet": { 11 | "MODEL_ID": "anthropic.claude-3-sonnet-20240229-v1:0", 12 | "NUM_DOCS_DEFAULT": 5, 13 | "TEMPERATURE_DEFAULT": 0.0, 14 | "ANSWER_LENGTH_DEFAULT": 200, 15 | "DOC_LENGTH_DEFAULT": 1000, 16 | "RELEVANCE_THRESHOLD_DEFAULT": 0.2 17 | }, 18 | "Bedrock: Claude Instant": { 19 | "MODEL_ID": "anthropic.claude-instant-v1", 20 | "NUM_DOCS_DEFAULT": 5, 21 | "TEMPERATURE_DEFAULT": 0.0, 22 | "ANSWER_LENGTH_DEFAULT": 200, 23 | "DOC_LENGTH_DEFAULT": 1000, 24 | "RELEVANCE_THRESHOLD_DEFAULT": 0.2 25 | }, 26 | "Bedrock: J2 Grande Instruct": { 27 | "MODEL_ID": "ai21.j2-grande-instruct", 28 | "NUM_DOCS_DEFAULT": 5, 29 | "TEMPERATURE_DEFAULT": 0.0, 30 | "ANSWER_LENGTH_DEFAULT": 200, 31 | "DOC_LENGTH_DEFAULT": 1000, 32 | "RELEVANCE_THRESHOLD_DEFAULT": 0.2 33 | }, 34 | "Bedrock: J2 Jumbo Instruct": { 35 | "MODEL_ID": "ai21.j2-jumbo-instruct", 36 | "NUM_DOCS_DEFAULT": 5, 37 | "TEMPERATURE_DEFAULT": 0.0, 38 | "ANSWER_LENGTH_DEFAULT": 200, 39 | "DOC_LENGTH_DEFAULT": 1000, 40 | "RELEVANCE_THRESHOLD_DEFAULT": 0.2 41 | }, 42 | "Bedrock: LLama2": { 43 | "MODEL_ID": "meta.llama2", 44 | "NUM_DOCS_DEFAULT": 5, 45 | "TEMPERATURE_DEFAULT": 0.0, 46 | "ANSWER_LENGTH_DEFAULT": 200, 47 | "DOC_LENGTH_DEFAULT": 1000, 48 | "RELEVANCE_THRESHOLD_DEFAULT": 0.2 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lab2/assets/lambda/sns_topic_lambda/sns_topic_lambda.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | Lambda that performs summarization with Bedrock 4 | """ 5 | 6 | ######################### 7 | # LIBRARIES & LOGGER 8 | ######################### 9 | import os 10 | import json 11 | import logging 12 | import sys 13 | 14 | import boto3 15 | 16 | LOGGER = logging.Logger("SNS TOPIC LAMBDA", level=logging.DEBUG) 17 | HANDLER = logging.StreamHandler(sys.stdout) 18 | HANDLER.setFormatter(logging.Formatter("%(levelname)s | %(name)s | %(message)s")) 19 | LOGGER.addHandler(HANDLER) 20 | 21 | 22 | ######################### 23 | # HANDLER 24 | ######################### 25 | 26 | 27 | def lambda_handler(event, context): 28 | topic_arn = os.environ["SNS_TOPIC_ARN"] 29 | body = json.loads(event["body"]) 30 | client = boto3.client("sns") 31 | if body["type"] == "PUBLISH": 32 | LOGGER.info(f"Publishing to topic: {topic_arn}") 33 | client.publish(TopicArn=topic_arn, Message=body["message"], Subject=body["subject"]) 34 | elif body["type"] == "SUBSCRIBE": 35 | # check if subscription already exists 36 | subs = client.list_subscriptions_by_topic(TopicArn=topic_arn) 37 | for sub in subs["Subscriptions"]: 38 | if sub["Endpoint"] == body["email"]: 39 | LOGGER.info(f"Subscription already exists for {body['email']}") 40 | return {'statusCode': 200} 41 | # Else subscribe email to topic 42 | LOGGER.info(f"Subscribing to topic: {topic_arn}") 43 | client.subscribe(TopicArn=topic_arn, Protocol="email", Endpoint=body["email"]) 44 | else: 45 | LOGGER.error(f"Invalid request type: {body['type']}") 46 | LOGGER.error("Valid request types: PUBLISH, SUBSCRIBE") 47 | return {'statusCode': 400} 48 | return {'statusCode': 200} 49 | -------------------------------------------------------------------------------- /lab2/config.yml: -------------------------------------------------------------------------------- 1 | # Settings for stack deployment 2 | # Choose the components you need for your demo, there should be only one value set to True per section 3 | 4 | stack_name: bdrkWorkshop # Name of your demo, will be used as stack name and prefix for resources 5 | 6 | authentication: 7 | MFA: False # Set to True/False to enable/disable multi-factor authentication 8 | 9 | streamlit: 10 | deploy_streamlit: True # Whether to deploy Streamlit frontend on ECS 11 | open_to_public_internet: True # Opens the Application Load Balancer to the internet 12 | # ip_address_allowed: [pl-4e2ece27] # List of IP addresses (cidr ranges) and prefix lists allowed to access the app in the ALB Security Group. If not set, SG is open to the internet 13 | # For Amazon employees, please look at: 14 | # https://dogfish.amazon.com/#/search?q=Unfabric&attr.scope=PublicIP for IP addresses 15 | # https://w.amazon.com/bin/view/AmazonPrefixListLookup for Prefix lists 16 | ecs_memory: 8192 # Memory of the ECS instance (Mb) 17 | ecs_cpu: 4096 # CPU of ECS instance 18 | cover_image_url: "https://reinvent.awsevents.com/content/dam/reinvent/2023/media/ripples/countdown-keyart.png" # default cover image on login page 19 | cover_image_login_url: "https://reinvent.awsevents.com/content/dam/reinvent/2023/media/ripples/countdown-keyart.png" 20 | assistant_avatar: "https://d1.awsstatic.com/products/bedrock/icon_64_amazonbedrock.302e08f0c3cd2a11d37eb3d77cb894bc5ceff8e4.png" # avatar of the chatbot (either URL or "assistant") 21 | 22 | bedrock: 23 | region: "us-west-2" # Region of Amazon Bedrock 24 | 25 | cloudfront: 26 | custom_header_name: "X-My-Custom-Header" # Name of the custom header to be used for authentication 27 | custom_header_value: "aijfoiwjeoijfawioejfoiwajefoiwjeofiwoefjaoiwjefooijawefoij" # Value of the custom header to be used for authentication 28 | -------------------------------------------------------------------------------- /lab2/assets/streamlit/src/components/sns_api.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper classes for LLM inference 3 | """ 4 | 5 | ######################### 6 | # IMPORTS & LOGGER 7 | ######################### 8 | 9 | from __future__ import annotations 10 | import os 11 | 12 | import requests 13 | 14 | ######################### 15 | # CONSTANTS 16 | ######################### 17 | 18 | API_URI = os.environ.get("API_URI") 19 | 20 | WS_SSL = (os.environ.get("WS_SSL", "True")) == "True" 21 | 22 | ######################### 23 | # HELPER FUNCTIONS 24 | ######################### 25 | 26 | 27 | def sns_topic_publish( 28 | subject: str, 29 | message: str, 30 | access_token: str, 31 | ) -> dict: 32 | params = {"subject": subject, "message": message, "type": "PUBLISH"} 33 | 34 | try: 35 | response = requests.post( 36 | url=API_URI + "/sns/put", 37 | json=params, 38 | stream=False, 39 | headers={"Authorization": access_token}, 40 | timeout=10, 41 | ) 42 | response = response 43 | return True, response 44 | except requests.RequestException as e: 45 | raise ValueError(f"Error making request to SNS API: {str(e)}") 46 | 47 | 48 | def sns_topic_subscribe_email( 49 | email: str, 50 | access_token: str, 51 | ) -> dict: 52 | params = {"email": email, "type": "SUBSCRIBE"} 53 | 54 | try: 55 | response = requests.post( 56 | url=API_URI + "/sns/put", 57 | json=params, 58 | stream=False, 59 | headers={"Authorization": access_token}, 60 | timeout=20, 61 | ) 62 | print(f"REPONSE: {response}") 63 | success = response.ok 64 | return success, response 65 | except requests.RequestException as e: 66 | raise ValueError(f"Error making request to SNS API: {str(e)}") 67 | -------------------------------------------------------------------------------- /lab2/assets/streamlit/src/components/utils_models.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | from typing import Any, Dict, List, Tuple 4 | 5 | BEDROCK_MODELS = [ 6 | "Bedrock: Claude 3 Sonnet", 7 | "Bedrock: Amazon Titan", 8 | ] 9 | # TODO - AFTER REINVENT - UNCOMMENT 10 | BEDROCK_MODELS_after_reinvent = [ 11 | "Bedrock: Claude 3 Sonnet" 12 | "Bedrock: Claude Instant", 13 | "Bedrock: J2 Grande Instruct", 14 | "Bedrock: J2 Jumbo Instruct", 15 | "Bedrock: Amazon Titan", 16 | "Bedrock: LLama2", 17 | ] 18 | FILTER_BEDROCK_MODELS = ["ALL"] + BEDROCK_MODELS 19 | 20 | 21 | def get_models_specs(path: Path) -> Tuple[List[str], Dict[str, Any]]: 22 | """ 23 | Get list of models displayed in the UI and their specs (i.e. their default parameters) 24 | 25 | Parameters 26 | ---------- 27 | sm_endpoints : dict 28 | Dictionary of Sagemaker deployed endpoints configurations. 29 | Format: {friendly_unique_name:{'endpoint_name':...,'container':...,'model_id':...}} 30 | path : 31 | os path 32 | 33 | Returns 34 | ------- 35 | MODEL_SPECS : dict 36 | dictionary of default parameters per model 37 | MODELS_DISPLAYED : list 38 | list of models displayed 39 | """ 40 | 41 | # default model specs 42 | with open(f"{path.parent.absolute()}/components/bedrock_model_specs.json") as f: 43 | model_specs = json.load(f) 44 | # with open( 45 | # f"{path.parent.absolute()}/components/sm_endpoints_model_specs.json" 46 | # ) as f: 47 | # sm_endpoints_model_specs = json.load(f) 48 | 49 | # sm_endpoints_friendly_names = list(sm_endpoints.keys()) 50 | 51 | # sm_endpoints_model_default_config = { 52 | # friendly_name: sm_endpoints_model_specs[model_spec["model_id"]] 53 | # for friendly_name, model_spec in sm_endpoints.items() 54 | # } 55 | # model_specs.update(sm_endpoints_model_default_config) 56 | 57 | # for key, value in sm_endpoints.items(): 58 | # model_specs[key] = sm_endpoints_model_specs[value["model_id"]] 59 | 60 | # models_displayed = BEDROCK_MODELS + sm_endpoints_friendly_names 61 | return BEDROCK_MODELS, model_specs 62 | -------------------------------------------------------------------------------- /lab2/assets/streamlit/src/data/customer_variables.txt: -------------------------------------------------------------------------------- 1 | Address: Email address of the customer 2 | OptOut: Has the customer opted out of marketing emails from the bank 3 | User.UserId: User id of the customer in the bank 4 | User.UserAttributes.PreferredChannel: Preferred communication channel chosen by the customer(Email/SMS etc.) 5 | User.UserAttributes.CLVTier: The CLV (Customer Lifetime Value) tier of the customer. This depicts how much the bank can plan to earn from the customer over the course of the relationship 6 | User.UserAttributes.MemberClass: The tier of the customer class. Eg: Gold, Platinum, Silver, Bronze etc. 7 | User.UserAttributes.Stage: At which the customer is in terms of customer journey. Retention means the customer might churn and is staying because bank has offered retention offers. Usage means the customer is a just a regular user with no forecast of churn. Upsell means the bank see the customer as potential interest to sell new products. 8 | User.UserAttributes.FirstName: First name of the customer. 9 | User.UserAttributes.LastName: Last name of the customer. 10 | User.UserAttributes.Age: Age of the customer. 11 | User.UserAttributes.Job: The job title of the customer. 12 | User.UserAttributes.Marital: Marital status of the customer 13 | User.UserAttributes.Education: number of years of formal education for the customer 14 | User.UserAttributes.DefaultScore: What the default score prediction for the customer. Higher means higher chance of defaulting 15 | User.UserAttributes.Housing: How many active home loans the customer has 16 | User.UserAttributes.Income: Annual income of the customer 17 | User.UserAttributes.Product: Id of the Recommended lending product, to be promoted to the customer 18 | User.UserAttributes.PreferredLanguage: Preferred language of communication chosen by the customer 19 | Attributes.LastEngagement: Number of days since last interacted/engaged with the customer. 20 | Metrics.ChurnPrediction: The probability of customer leaving the bank 21 | Metrics.UpsellPrediction: The probablity the customer will buy a new product if offered. 22 | Metrics.OptimalSendHour: Optimal hour of the day for contacting via email. 23 | Demographic.Timezone: Timezone the customer is in. 24 | Demographic.Locale: Language/Locale of the customer -------------------------------------------------------------------------------- /lab2/assets/streamlit/src/data/df_item_banking.csv: -------------------------------------------------------------------------------- 1 | itemId,Name,Title,Description,Key Features,Key Benefits,Great For 2 | L001,Unsecured Installment Loan,Lending Solutions for Accomplishing Your Goals:,An Unsecured Installment Loan from FNB1 can provide you with the borrowing power you need to meet your financial objectives. These loans are repaid over a set time frame (term) with a specific number of payments.,"Borrow anywhere from $2,500 - $25,000|One-time, lump-sum distribution of loan proceeds|Fixed repayment schedule|Available with a fixed rate|Interest rate discount available when you set up automatic monthly payments from an FNB Deposit account|Flexible repayment terms","Immediate spending power|Flexible and convenient payment methods including by mail, in-branch, online or automatic debit from your deposit account","Larger, one-time purchases|Home renovations|New appliances|Debt consolidation" 3 | L002,Secured Installment Loan,Enjoy Real Flexibility,"A Secured Line of Credit from FNB1 gives you the flexibility to borrow up to your credit limit at any time. Whether you use your line for overdraft protection or just as a cushion for those unexpected expenses, a line of credit provides you the borrowing power you need to help meet life's financial demands.","Secured with an FNB Savings Account or Certificate of Deposit2|Flexibility to borrow as needed, repay and borrow again up to your credit limit3|Borrow up to $250,0004|Variable interest rate tied to the U.S. Prime Rate5|Access your funds via check or online transfer",Asset protection through secured loan|Lower interest rates|Flexible repayment options,Providing overdraft protection for your checking account|Covering unexpected expenses|Providing liquidity to bridge the gap between when your bills are due and when you get paid 4 | CC003,FNB SmartRateSM Credit Card,Get More Savings with Lower Rates,"With the FNB SmartRate Card, you can get more savings with our lowest interest rate. You can use your card anywhere Visa is accepted to conduct everyday purchases. For added convenience, consolidate your monthly bills on one statement for one monthly payment.",,No Annual Fee (Please review rates and fees in the pricing information link below for costs associated with this credit card)|Competitive Variable APR,"Everyday purchases, from groceries to gas to monthly bills|Consolidating your monthly expenses on one convenient monthly bill|Saving on interest every time you use your card to make a purchase" -------------------------------------------------------------------------------- /lab2/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["poetry-core>=1.7.0"] 3 | build-backend = "poetry.core.masonry.api" 4 | 5 | [tool.poetry] 6 | name = "reinvent-bedrock-workshop" 7 | version = "0.2.0" 8 | description = "Bedrock & Build Prompt Template workshop for re:Invent-23" 9 | authors = [ 10 | "Philipp Kaindl ", 11 | "Akarsha Sehwag ", 12 | "Olivier Boder ", 13 | "Vikesh Pandey ", 14 | "Tingyi Li ", 15 | ] 16 | license = "Amazon Software License" 17 | 18 | [tool.poetry.dependencies] 19 | python = "~3.9.0" 20 | aws-cdk-lib = ">=2.99.0" 21 | constructs = "^10.0.0" 22 | boto3 = "~1.27.0" 23 | pyyaml = "^6.0.0" 24 | aws-cdk-aws-apigatewayv2-alpha = "^2.0" 25 | aws-cdk-aws-apigatewayv2-integrations-alpha = "^2.0.0" 26 | aws-cdk-aws-apigatewayv2-authorizers-alpha = "^2.0.0" 27 | requests = "~2.31.0" 28 | aws-lambda-powertools = {extras = ["validation"], version = "^2.25.1"} 29 | 30 | 31 | [tool.poetry.group.dev.dependencies] 32 | pre-commit = "^3.3.3" 33 | black = "^23.7.0" 34 | ruff = "^0.0.282" 35 | isort = "^5.12.0" 36 | ipykernel = "^6.25.2" 37 | 38 | 39 | [tool.poetry.group.test.dependencies] 40 | pytest = "^7.0.0" 41 | hypothesis = "^6.87.3" 42 | 43 | [tool.isort] 44 | profile = "black" 45 | multi_line_output = 3 46 | include_trailing_comma = true 47 | force_grid_wrap = 0 48 | use_parentheses = true 49 | ensure_newline_before_comments = true 50 | line_length = 120 51 | skip_gitignore = true 52 | known_first_party = ["utils", "model", "retriever", "messaging"] 53 | 54 | [tool.ruff] 55 | select = ["E", "F", "C", "W", "R", "B", "Q"] 56 | line-length = 120 57 | target-version="py39" 58 | exclude = [ 59 | ".bzr", 60 | ".direnv", 61 | ".eggs", 62 | ".git", 63 | ".hg", 64 | ".mypy_cache", 65 | ".nox", 66 | ".pants.d", 67 | ".pytype", 68 | ".ruff_cache", 69 | ".svn", 70 | ".tox", 71 | "__pypackages__", 72 | "_build", 73 | "build", 74 | "dist", 75 | "venv", 76 | ".venv", 77 | ] 78 | 79 | [tool.black] 80 | target-version = ['py39'] 81 | line-length = 120 82 | 83 | [tool.pytest.ini_options] 84 | addopts = [ 85 | "--import-mode=importlib", 86 | ] 87 | pythonpath = [ 88 | ".", # Adds the project's root directory and makes the `infra` package importable 89 | "./assets/layers", 90 | "./assets/lambda", 91 | ] 92 | norecursedirs = [ 93 | "cdk.out" 94 | ] 95 | -------------------------------------------------------------------------------- /lab1/bank-call-centre-transcript.txt: -------------------------------------------------------------------------------- 1 | Customer: Hi there, I'm having a problem with my account. 2 | Agent: Hi! I'm sorry to hear that. What's happening? 3 | Customer: I am not able to login to my account and also not able to use my debit card. I have tried resetting my password but the issue persists. 4 | Agent: Hmm, that's not good. Let's try some troubleshooting steps. Can you go to the website and try resetting the password from there and see if you get password reset email and try following the steps from there? 5 | Customer: Ok, i tried it and received the password reset mail. I have reset my password but the login still fails. 6 | Agent: Okay, from your login history i can see you tried few times last week and your account got locked out. You also tried few incorrect pin attempts on your debit card and that is how your debit card also got blocked. Can you confirm if you performed these actions? 7 | Customer: I did try to login multiple times and failed. but i did not use my debit card at all last week. 8 | Agent: Alright, can you confirm the last debit card transaction you did with the card? 9 | Customer: Okay, I used the card for buying groceries last week and did a purchase of around USD 50. 10 | Agent: Okay, i can see that in my system as well but then i see 4 failed debit card usage attempts somewhere in eastern europe over the last 4 days. Was that you? 11 | Customer: No that was not me. 12 | Agent: I see. It looks like you had an unauthorized usage on your card. I am going to permanently block your card and issue you a new one. 13 | Customer: Do I need to come to bank branch to get the new card? 14 | Agent: No, You will receive it by mail in next few days. Your debit card pin will be mailed separately to you. 15 | Customer: Okay, and what about the login issue? 16 | Agent: Let me reset your credentials from the backend. since your debit card was compromised, your account might also be at risk. So i am going to reset your account credentials. from the backend. 17 | Customer: How long will it take to get my new debit card and login credentials? 18 | Agent: For the debit card, It depends on the speed of mail delivery. but it usually takes up to 5 business days for both debit card and pin to arrive at your address. 19 | Customer: And what about new login credentials? 20 | Agent: Within next hour, you will receive an email on your registered email id with new login credentials. It will have your temporary password and link to login. Upon clicking the link, you will be asked to put in your temporary password. Then the page will force you to create a new password. As soon you enter a new password, your new credentials will active immediately for use. 21 | Customer: Alright, thanks for your help. 22 | Agent: No problem, happy to help. Is there anything else I can assist you with? 23 | Customer: No, that's all for now. 24 | Agent: Alright, have a great day and thanks for banking with us! -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lab2/assets/streamlit/src/data/df_segment_data.csv: -------------------------------------------------------------------------------- 1 | Address,OptOut,User.UserId,User.UserAttributes.PreferredChannel,User.UserAttributes.CLVTier,User.UserAttributes.MemberClass,User.UserAttributes.Stage,User.UserAttributes.FirstName,User.UserAttributes.LastName,User.UserAttributes.Age,User.UserAttributes.Job,User.UserAttributes.Marital,User.UserAttributes.Education,User.UserAttributes.DefaultScore,User.UserAttributes.Housing,User.UserAttributes.Income,User.UserAttributes.Product,User.UserAttributes.PreferredLanguage,User.UserAttributes.CustomerSinceYear,Attributes.LastEngagement,Metrics.ChurnPrediction,Metrics.UpsellPrediction,Metrics.OptimalSendHour,Demographic.Timezone,Demographic.Locale 2 | triri@yahoo.com,FALSE,6680817099961912103,EMAIL,Low,Bronze,Usage,Tristan,Nguyen,66,Software Engineer III,married,7,2,1,343723.23,L001,English,1974,36,0.6,0.66,10,Europe/Bucharest,Croatian 3 | robertsemily@example.org,FALSE,25881557449465934,EMAIL,Medium,Silver,Retention,Lois,Petkovic,73,VP Sales,widowed,13,1,2,46987.46,L001,Mongolian,2021,91,0.56,0.92,11,Europe/Stockholm,Zulu 4 | cabreraearl@example.net,TRUE,5580214410873154371,EMAIL,Medium,Bronze,Upsell,Emelina,Winsome,31,Registered Nurse,single,6,9,1,125825.56,L001,Thai,2018,15,0.44,0.42,14,Asia/Chongqing,Portuguese 5 | vholmes@example.net,FALSE,5632645845239563973,EMAIL,Low,Bronze,Usage,Randolph,Boylin,11,Operator,widowed,17,7,2,757496.85,L001,Lao,1997,22,0.63,0.53,4,Europe/Paris,Chinese 6 | michael58@example.com,TRUE,4749272229605988173,EMAIL,Low,Default_memberClass,Usage,Bronson,Matitiaho,34,Geologist IV,single,10,1,1,195901.76,L001,Italian,1963,89,0.04,0.53,15,America/Lima,Afrikaans 7 | joshuasharp@example.com,FALSE,7383582213426254551,EMAIL,High,Gold,Usage,Margie,Yakubovics,60,Nuclear Power Engineer,divorced,18,1,2,923891.63,L001,Italian,2010,88,0.76,0.33,17,America/Sao_Paulo,Tetum 8 | michaelgarcia@example.org,TRUE,1513831748228506677,EMAIL,Low,Gold,Retention,Karoly,Adolf,38,Research Nurse,widowed,19,9,2,547830.9,L001,Swedish,1973,7,0.16,0.84,1,Asia/Yekaterinburg,Maltese 9 | suzanne76@example.org,TRUE,2070509311478048601,EMAIL,High,Default_memberClass,Usage,Rani,Cursey,127,Marketing Assistant,single,4,4,0,560290.39,L001,Haitian Creole,1923,86,0.78,0.08,16,Africa/Gaborone,Gujarati 10 | christina21@example.com,TRUE,1430224993758423118,EMAIL,Medium,Silver,Retention,Griswold,Vasilic,28,Assistant Professor,married,10,8,2,145863.7,L001,Pashto,2003,79,0.49,0.75,20,America/Guatemala,Albanian 11 | anthonylloyd@example.net,FALSE,4396704326082494968,EMAIL,Low,Silver,Upsell,Andrea,Donaldson,117,Systems Administrator III,married,17,2,0,185355.52,L001,Azeri,1927,79,0.26,0.33,0,Asia/Shanghai,Dutch 12 | martin43@example.net,FALSE,5391917930340855942,EMAIL,Medium,Default_memberClass,Upsell,Marcille,Guarin,131,VP Sales,widowed,3,2,2,686433.9,L001,Assamese,1991,45,0.97,0.25,21,Europe/Stockholm,Latvian 13 | ashleemercado@example.com,FALSE,966216635981625228,EMAIL,High,Default_memberClass,Upsell,Tallia,New,26,Senior Editor,married,2,6,1,465705.61,L001,Italian,1932,49,0.55,0.51,23,Asia/Shanghai,Dari 14 | johnjames@example.com,TRUE,8283521804018276247,EMAIL,Low,Bronze,Upsell,Ali,Puzey,85,Accounting Assistant III,married,5,8,2,637694.98,L001,Thai,2008,37,0.52,0.05,21,Africa/Ouagadougou,Icelandic 15 | pam91@example.com,TRUE,6429655285768218777,EMAIL,Medium,Silver,Usage,Cristin,Todari,13,Physical Therapy Assistant,single,13,0,2,807000.01,L001,Norwegian,2003,31,0.11,0.57,18,America/New_York,Dutch 16 | philip41@example.com,FALSE,6997528729987718603,EMAIL,Medium,Gold,Usage,Ferd,Hanning,38,Senior Cost Accountant,single,1,0,0,294734.16,L001,Aymara,1944,85,0.23,0.92,12,Asia/Tokyo,Swati -------------------------------------------------------------------------------- /lab2/assets/streamlit/src/data/products.json: -------------------------------------------------------------------------------- 1 | { 2 | "products": [ 3 | { 4 | "id": "L001", 5 | "Name": "Unsecured Installment Loan", 6 | "Title": "Lending Solutions for Accomplishing Your Goals:", 7 | "Description": "An Unsecured Installment Loan from FNB1 can provide you with the borrowing power you need to meet your financial objectives. These loans are repaid over a set time frame (term) with a specific number of payments.", 8 | "Key Features": [ 9 | "Borrow anywhere from $2,500 - $25,000", 10 | "One-time, lump-sum distribution of loan proceeds", 11 | "Fixed repayment schedule", 12 | "Available with a fixed rate", 13 | "Interest rate discount available when you set up automatic monthly payments from an FNB Deposit account", 14 | "Flexible repayment terms" 15 | ], 16 | "Key Benefits": [ 17 | "Immediate spending power", 18 | "Flexible and convenient payment methods including by mail, in-branch, online or automatic debit from your deposit account" 19 | ], 20 | "Great For": [ 21 | "Larger, one-time purchases", 22 | "Home renovations", 23 | "New appliances", 24 | "Debt consolidation" 25 | ] 26 | }, 27 | { 28 | "id": "L002", 29 | "Name": "Secured Installment Loan", 30 | "Title": "Enjoy Real Flexibility:", 31 | "Description": "A Secured Line of Credit from FNB1 gives you the flexibility to borrow up to your credit limit at any time. Whether you use your line for overdraft protection or just as a cushion for those unexpected expenses, a line of credit provides you the borrowing power you need to help meet life's financial demands.", 32 | "Key Features": [ 33 | "Secured with an FNB Savings Account or Certificate of Deposit2", 34 | "Flexibility to borrow as needed, repay and borrow again up to your credit limit3", 35 | "Borrow up to $250,0004", 36 | "Variable interest rate tied to the U.S. Prime Rate5", 37 | "Access your funds via check or online transfer" 38 | ], 39 | "Great For": [ 40 | "Providing overdraft protection for your checking account", 41 | "Covering unexpected expenses", 42 | "Providing liquidity to bridge the gap between when your bills are due and when you get paid" 43 | ] 44 | }, 45 | { 46 | "id": "CC003", 47 | "Name": "FNB SmartRateSM Credit Card", 48 | "Title": "Get More Savings with Lower Rates", 49 | "Description": "With the FNB SmartRate Card, you can get more savings with our lowest interest rate. You can use your card anywhere Visa is accepted to conduct everyday purchases. For added convenience, consolidate your monthly bills on one statement for one monthly payment.", 50 | "Key Features": [ 51 | "No Annual Fee (Please review rates and fees in the pricing information link below for costs associated with this credit card)", 52 | "Competitive Variable APR" 53 | ], 54 | "Great For": [ 55 | "Everyday purchases, from groceries to gas to monthly bills", 56 | "Consolidating your monthly expenses on one convenient monthly bill", 57 | "Saving on interest every time you use your card to make a purchase" 58 | ] 59 | } 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /lab2/infra/bdrk_reinvent_stack.py: -------------------------------------------------------------------------------- 1 | """ 2 | bdrk_reinvent stack 3 | """ 4 | 5 | from typing import Any, Dict 6 | 7 | from aws_cdk import Aws 8 | from aws_cdk import CfnOutput as output 9 | from aws_cdk import RemovalPolicy, Stack, Tags 10 | 11 | from constructs import Construct 12 | 13 | from infra.constructs.bdrk_reinvent_api import bdrk_reinventAPIConstructs 14 | from infra.constructs.bdrk_reinvent_layers import bdrk_reinventLambdaLayers 15 | from infra.stacks.bdrk_reinvent_streamlit import bdrk_reinventStreamlitStack 16 | 17 | sm_endpoints = {} 18 | 19 | 20 | class bdrkReinventStack(Stack): 21 | """ 22 | bdrk_reinvent stack 23 | """ 24 | 25 | def __init__(self, scope: Construct, stack_name: str, config: Dict[str, Any], **kwargs) -> None: # noqa: C901 26 | super().__init__(scope, stack_name, **kwargs) 27 | 28 | ## **************** Lambda layers **************** 29 | 30 | self.layers = bdrk_reinventLambdaLayers(self, f"{stack_name}-layers", stack_name=stack_name) 31 | 32 | ## ********** Bedrock configs *********** 33 | bedrock_region = kwargs["env"].region 34 | bedrock_role_arn = None 35 | 36 | if "bedrock" in config: 37 | if "region" in config["bedrock"]: 38 | bedrock_region = ( 39 | kwargs["env"].region if config["bedrock"]["region"] == "None" else config["bedrock"]["region"] 40 | ) 41 | 42 | ## ********** Authentication configs *********** 43 | mfa_enabled = True 44 | if "authentication" in config: 45 | if "MFA" in config["authentication"]: 46 | mfa_enabled = config["authentication"]["MFA"] 47 | 48 | ## **************** API Constructs **************** 49 | self.api_constructs = bdrk_reinventAPIConstructs( 50 | self, 51 | f"{stack_name}-API", 52 | stack_name=stack_name, 53 | layers=self.layers, 54 | bedrock_region=bedrock_region, 55 | bedrock_role_arn=bedrock_role_arn, 56 | mfa_enabled=mfa_enabled, 57 | ) 58 | 59 | ## **************** Streamlit NestedStack **************** 60 | if config["streamlit"]["deploy_streamlit"]: 61 | self.streamlit_constructs = bdrk_reinventStreamlitStack( 62 | self, 63 | f"{stack_name}-STREAMLIT", 64 | stack_name=stack_name, 65 | client_id=self.api_constructs.client_id, 66 | api_uri=self.api_constructs.api_uri, 67 | ecs_cpu=config["streamlit"]["ecs_cpu"], 68 | ecs_memory=config["streamlit"]["ecs_memory"], 69 | cover_image_url=config["streamlit"]["cover_image_url"], 70 | cover_image_login_url=config["streamlit"]["cover_image_login_url"], 71 | assistant_avatar=config["streamlit"]["assistant_avatar"], 72 | open_to_public_internet=config["streamlit"]["open_to_public_internet"], 73 | ip_address_allowed=config["streamlit"].get("ip_address_allowed"), 74 | custom_header_name=config["cloudfront"]["custom_header_name"], 75 | custom_header_value=config["cloudfront"]["custom_header_value"], 76 | ) 77 | 78 | self.alb_dns_name = output( 79 | self, 80 | id="AlbDnsName", 81 | value=self.streamlit_constructs.alb.load_balancer_dns_name, 82 | ) 83 | 84 | self.cloudfront_distribution_name = output( 85 | self, 86 | id="cloudfront_distribution_domain_name", 87 | value=self.streamlit_constructs.cloudfront.domain_name, 88 | ) 89 | 90 | ## **************** Tags **************** 91 | Tags.of(self).add("StackName", stack_name) 92 | Tags.of(self).add("Team", "Bedrock Workshop") 93 | -------------------------------------------------------------------------------- /lab2/assets/streamlit/src/components/genai_api.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper classes for LLM inference 3 | """ 4 | 5 | ######################### 6 | # IMPORTS & LOGGER 7 | ######################### 8 | 9 | from __future__ import annotations 10 | 11 | import json 12 | import os 13 | from typing import Callable 14 | 15 | import requests 16 | 17 | ######################### 18 | # CONSTANTS 19 | ######################### 20 | 21 | API_URI = os.environ.get("API_URI") 22 | 23 | # DEFAULT_NEGATIVE_ANSWER_QUESTION = "Could not answer based on the provided documents. Please rephrase your question, reduce the relevance threshold, or ask another question." # noqa: E501 24 | # DEFAULT_NEGATIVE_ANSWER_SUMMARY = "Could not summarize the document." # noqa: E501 25 | # WS_SSL = (os.environ.get("WS_SSL", "True")) == "True" 26 | 27 | ######################### 28 | # HELPER FUNCTIONS 29 | ######################### 30 | 31 | 32 | def invoke_content_creation( 33 | prompt: str, 34 | model_id: int, 35 | access_token: str, 36 | answer_length: int = 4096, 37 | temperature: float = 0.0, 38 | ) -> str: 39 | """ 40 | Run LLM to generate content via API 41 | """ 42 | 43 | params = { 44 | "query": prompt, 45 | "type": "content_generation", 46 | "model_params": { 47 | "model_id": model_id, 48 | "answer_length": answer_length, 49 | "temperature": temperature, 50 | }, 51 | } 52 | try: 53 | response = requests.post( 54 | url=API_URI + "/content/bedrock", 55 | json=params, 56 | stream=False, 57 | headers={"Authorization": access_token}, 58 | timeout=60, # add a timeout parameter of 10 seconds 59 | ) 60 | print(response) 61 | # response.raise_for_status() # This will raise an HTTPError if the HTTP request returned an unsuccessful status code 62 | response = json.loads(response.text) 63 | return response 64 | except requests.RequestException as e: 65 | # Handle exception as needed 66 | raise ValueError(f"Error making request to LLM API: {str(e)}") 67 | 68 | 69 | def invoke_dynamo_put( 70 | item: dict, 71 | access_token: str, 72 | ) -> str: 73 | """ 74 | Put the json item into DynamoDB via an API endpoint. 75 | """ 76 | 77 | data = { 78 | "item": item, 79 | "type": "PUT", 80 | } 81 | 82 | headers = {"Authorization": access_token} 83 | 84 | try: 85 | response = requests.post( 86 | url=API_URI + "/dynamo/put", 87 | json=data, # Use json=data to send as JSON payload 88 | headers=headers, 89 | timeout=10, 90 | ) 91 | response.raise_for_status() # This will raise an HTTPError if the HTTP request returned an unsuccessful status code 92 | return response.json() 93 | except requests.RequestException as e: 94 | # Handle exception as needed 95 | raise ValueError(f"Error making request to Dynamo API: {str(e)}") 96 | 97 | except json.JSONDecodeError: 98 | # Handle JSON parsing exception as needed 99 | raise ValueError("Received a non-JSON response from the server.") 100 | 101 | 102 | def invoke_dynamo_get( 103 | params: dict, 104 | access_token: str, 105 | ) -> str: 106 | """ 107 | Get the elements from DynamoDB via an API endpoint. 108 | """ 109 | 110 | data = { 111 | "filter_params": params, 112 | "type": "GET", 113 | } 114 | 115 | headers = {"Authorization": access_token} 116 | 117 | try: 118 | response = requests.get( 119 | url=API_URI + "/dynamo/get", json=data, headers=headers, timeout=10 120 | ) 121 | response.raise_for_status() # This will raise an HTTPError if the HTTP request returned an unsuccessful status code 122 | return response 123 | except requests.RequestException as e: 124 | # Handle exception as needed 125 | raise ValueError(f"Error making request to Dynamo API: {str(e)}") 126 | 127 | 128 | def invoke_dynamo_delete( 129 | params: dict, 130 | access_token: str, 131 | ) -> dict: 132 | """ 133 | Delete the specified elements from DynamoDB via an API endpoint. 134 | """ 135 | print("invoke_dynamo_delete") 136 | print(params["session_id"], type(params)) 137 | data = { 138 | "item": params, 139 | "type": "DELETE", 140 | } 141 | 142 | headers = {"Authorization": access_token} 143 | 144 | try: 145 | response = requests.delete( 146 | url=f"{API_URI}/dynamo/delete", json=data, headers=headers, timeout=10 147 | ) 148 | response.raise_for_status() # This will raise an HTTPError if the HTTP request returned an unsuccessful status code 149 | return response 150 | except requests.RequestException as e: 151 | # Handle exception as needed 152 | raise ValueError(f"Error making request to Dynamo API: {str(e)}") 153 | -------------------------------------------------------------------------------- /lab2/run.sh: -------------------------------------------------------------------------------- 1 | 2 | #!/bin/bash 3 | 4 | # Specify the desired volume size in GiB as a command line argument. If not specified, default to 20 GiB. 5 | SIZE=50 6 | 7 | 8 | start_time_resize=$(date +%s) 9 | 10 | # Get the ID of the environment host Amazon EC2 instance. 11 | TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 60") 12 | INSTANCEID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" -v http://169.254.169.254/latest/meta-data/instance-id 2> /dev/null) 13 | REGION=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" -v http://169.254.169.254/latest/meta-data/placement/region 2> /dev/null) 14 | 15 | # Get the ID of the Amazon EBS volume associated with the instance. 16 | VOLUMEID=$(aws ec2 describe-instances \ 17 | --instance-id $INSTANCEID \ 18 | --query "Reservations[0].Instances[0].BlockDeviceMappings[0].Ebs.VolumeId" \ 19 | --output text \ 20 | --region $REGION) 21 | 22 | # Resize the EBS volume. 23 | aws ec2 modify-volume --volume-id $VOLUMEID --size $SIZE 24 | 25 | # Wait for the resize to finish. 26 | while [ \ 27 | "$(aws ec2 describe-volumes-modifications \ 28 | --volume-id $VOLUMEID \ 29 | --filters Name=modification-state,Values="optimizing","completed" \ 30 | --query "length(VolumesModifications)"\ 31 | --output text)" != "1" ]; do 32 | sleep 1 33 | done 34 | 35 | # Check if we're on an NVMe filesystem 36 | if [[ -e "/dev/xvda" && $(readlink -f /dev/xvda) = "/dev/xvda" ]] 37 | then 38 | # Rewrite the partition table so that the partition takes up all the space that it can. 39 | sudo growpart /dev/xvda 1 40 | # Expand the size of the file system. 41 | # Check if we're on AL2 or AL2023 42 | STR=$(cat /etc/os-release) 43 | SUBAL2="VERSION_ID=\"2\"" 44 | SUBAL2023="VERSION_ID=\"2023\"" 45 | if [[ "$STR" == *"$SUBAL2"* || "$STR" == *"$SUBAL2023"* ]] 46 | then 47 | sudo xfs_growfs -d / 48 | else 49 | sudo resize2fs /dev/xvda1 50 | fi 51 | 52 | else 53 | # Rewrite the partition table so that the partition takes up all the space that it can. 54 | sudo growpart /dev/nvme0n1 1 55 | 56 | # Expand the size of the file system. 57 | # Check if we're on AL2 or AL2023 58 | STR=$(cat /etc/os-release) 59 | SUBAL2="VERSION_ID=\"2\"" 60 | SUBAL2023="VERSION_ID=\"2023\"" 61 | if [[ "$STR" == *"$SUBAL2"* || "$STR" == *"$SUBAL2023"* ]] 62 | then 63 | sudo xfs_growfs -d / 64 | else 65 | sudo resize2fs /dev/nvme0n1p1 66 | fi 67 | fi 68 | 69 | # End time and duration 70 | end_time_resize=$(date +%s) 71 | duration_resize=$((end_time_resize - start_time_resize)) 72 | echo "Time taken to resize EBS Volume: $duration_resize seconds" 73 | 74 | # sleep for 60 seconds 75 | sleep 60 76 | 77 | # Exit if any command fails 78 | set -e 79 | 80 | start_time=$(date +%s) 81 | 82 | # Check if Miniconda is installed and install it if not 83 | if ! command -v conda &> /dev/null; then 84 | echo "Miniconda is not installed. Installing Miniconda..." 85 | wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh 86 | bash Miniconda3-latest-Linux-x86_64.sh -b -p $HOME/miniconda 87 | echo "Miniconda installed successfully." 88 | fi 89 | 90 | # Initialize Miniconda environment 91 | export PATH="$HOME/miniconda/bin:$PATH" 92 | 93 | # Initialize Conda in your shell (modify this line for different shells) 94 | source $HOME/miniconda/etc/profile.d/conda.sh 95 | 96 | 97 | echo "Initialized Conda in the shell. Currently in directory: $PWD" 98 | 99 | # Start time 100 | start_time_layers=$(date +%s) 101 | 102 | # Create and activate a separate Conda environment for Lambda Layer 103 | conda create -n lambda_layer_env python=3.9 -y 104 | conda activate lambda_layer_env 105 | 106 | # Install necessary packages in this environment 107 | pip install boto3 108 | 109 | # Prepare directory for Lambda Layer 110 | LAYER_DIR="./assets/layers/" 111 | mkdir -p $LAYER_DIR/python 112 | 113 | # Package the environment into a zip file 114 | cp -r $HOME/miniconda/envs/lambda_layer_env/lib/python3.9/site-packages/* $LAYER_DIR/python/ 115 | cd $LAYER_DIR 116 | zip -qr bedrock-compatible-sdk.zip python 117 | if [ $? -eq 0 ]; then 118 | echo "Zip file created successfully." 119 | rm -rf python 120 | echo "Python directory removed." 121 | else 122 | echo "There was an error creating the zip file." 123 | fi 124 | 125 | echo "Zipped the layer. Current directory is $PWD" 126 | 127 | # Move the zip file to the right location 128 | # mv bedrock-compatible-sdk.zip ../ 129 | 130 | # Cleanup (optional) 131 | conda deactivate 132 | conda env remove -n lambda_layer_env -y 133 | 134 | 135 | # End time and duration 136 | end_time_layers=$(date +%s) 137 | duration_layers=$((end_time_layers - start_time_layers)) 138 | echo "Time taken to build layer: $duration_layers seconds" 139 | 140 | echo "Current directory is $PWD" 141 | echo "Moving back to root dir of lab2" 142 | 143 | cd ../../ 144 | 145 | # Create and activate a Conda environment using Python 3.9 146 | conda create -n bdrkenv python=3.9 -y 147 | conda activate bdrkenv 148 | 149 | pip install poetry==1.7.0 150 | 151 | poetry install 152 | 153 | # sudo npm install -g aws-cdk@latest --force 154 | 155 | # cd ./assets/streamlit 156 | 157 | # poetry install 158 | 159 | # cd ../../ 160 | 161 | cdk bootstrap 162 | 163 | cdk deploy --require-approval=never 164 | 165 | # End time and duration 166 | end_time=$(date +%s) 167 | duration=$((end_time - start_time)) 168 | echo "Time taken to build everything: $duration seconds" -------------------------------------------------------------------------------- /lab2/assets/lambda/genai/bedrock_content_generation_lambda/bedrock_content_generation_lambda.py: -------------------------------------------------------------------------------- 1 | """ 2 | Lambda that performs email content generation with Bedrock 3 | """ 4 | 5 | ######################### 6 | # LIBRARIES & LOGGER 7 | ######################### 8 | 9 | import json 10 | import logging 11 | import os 12 | import sys 13 | from datetime import datetime, timezone 14 | 15 | import boto3 16 | from botocore.config import Config 17 | 18 | LOGGER = logging.Logger("Content-generation", level=logging.DEBUG) 19 | HANDLER = logging.StreamHandler(sys.stdout) 20 | HANDLER.setFormatter(logging.Formatter("%(levelname)s | %(name)s | %(message)s")) 21 | LOGGER.addHandler(HANDLER) 22 | 23 | 24 | ######################### 25 | # HELPER 26 | ######################### 27 | BEDROCK_ROLE_ARN = os.environ["BEDROCK_ROLE_ARN"] 28 | BEDROCK_CONFIG = Config(connect_timeout=60, read_timeout=60, retries={"max_attempts": 10}) 29 | 30 | MODELS_MAPPING = { 31 | "Bedrock: Amazon Titan": "amazon.titan-text-express-v1", 32 | "Bedrock: Claude 3 Sonnet": "anthropic.claude-3-sonnet-20240229-v1:0" 33 | } 34 | 35 | 36 | def create_bedrock_client(): 37 | """ 38 | Creates a Bedrock client using the specified region and configuration. 39 | 40 | Returns: 41 | A tuple containing the Bedrock client and the expiration time (which is None). 42 | """ 43 | LOGGER.info("Using bedrock client from same account.") 44 | bedrock_client = boto3.client( 45 | service_name="bedrock-runtime", 46 | region_name=os.environ["BEDROCK_REGION"], 47 | config=BEDROCK_CONFIG, 48 | ) 49 | expiration = None 50 | LOGGER.info("Successfully set bedrock client") 51 | 52 | return bedrock_client, expiration 53 | 54 | 55 | BEDROCK_CLIENT, EXPIRATION = create_bedrock_client() 56 | 57 | 58 | def verify_bedrock_client(): 59 | """ 60 | Verifies the Bedrock client by checking if the token has expired or not. 61 | 62 | Returns: 63 | bool: True if the Bedrock client is verified, False otherwise. 64 | """ 65 | if EXPIRATION is not None: 66 | now = datetime.now(timezone.utc) 67 | LOGGER.info(f"Bedrock token expires in {(EXPIRATION - now).total_seconds()}s") 68 | if (EXPIRATION - now).total_seconds() < 60: 69 | return False 70 | return True 71 | 72 | 73 | ######################### 74 | # HANDLER 75 | ######################### 76 | 77 | 78 | def lambda_handler(event, context): 79 | """ 80 | Lambda handler 81 | """ 82 | LOGGER.info("Starting execution of lambda_handler()") 83 | 84 | ### PREPARATIONS 85 | # Convert the 'body' string to a dictionary 86 | body_data = json.loads(event["body"]) 87 | 88 | # Extract the 'query' value 89 | query_value = body_data["query"] 90 | 91 | # Extract the 'model_params' value 92 | model_params_value = body_data["model_params"] 93 | 94 | # get fixed model params 95 | MODEL_ID = MODELS_MAPPING[model_params_value["model_id"]] 96 | LOGGER.info(f"MODEL_ID: {MODEL_ID}") 97 | 98 | if "claude-3" in MODEL_ID: 99 | model_config_path = f"model_configs/{MODEL_ID[:-2]}.json" 100 | else: 101 | model_config_path = f"model_configs/{MODEL_ID}.json" 102 | with open(model_config_path) as f: 103 | fixed_params = json.load(f) 104 | 105 | # load variable model params 106 | amazon_flag = False 107 | model_params = {} 108 | if MODEL_ID.startswith("amazon"): 109 | model_params = { 110 | "maxTokenCount": model_params_value["answer_length"], 111 | "stopSequences": fixed_params["STOP_WORDS"], 112 | "temperature": model_params_value["temperature"], 113 | "topP": fixed_params["TOP_P"], 114 | } 115 | amazon_flag = True 116 | elif "claude-3" in MODEL_ID: 117 | model_params = { 118 | "max_tokens": model_params_value["answer_length"], 119 | "temperature": model_params_value["temperature"], 120 | "top_p": fixed_params["TOP_P"], 121 | } 122 | message = {"role": "user", 123 | "content": [ 124 | {"type": "text", "text": query_value} 125 | ] 126 | } 127 | messages = [message] 128 | elif MODEL_ID.startswith("anthropic"): 129 | model_params = { 130 | "max_tokens_to_sample": model_params_value["answer_length"], 131 | "temperature": model_params_value["temperature"], 132 | "top_p": fixed_params["TOP_P"], 133 | "stop_sequences": fixed_params["STOP_WORDS"], 134 | } 135 | query_value = f"\n\nHuman:{query_value}\n\nAssistant:" 136 | 137 | LOGGER.info(f"MODEL_PARAMS: {model_params}") 138 | 139 | if not verify_bedrock_client(): 140 | LOGGER.info("Bedrock client expired, will refresh token.") 141 | global BEDROCK_CLIENT, EXPIRATION 142 | BEDROCK_CLIENT, EXPIRATION = create_bedrock_client() 143 | 144 | accept = "application/json" 145 | contentType = "application/json" 146 | 147 | if amazon_flag == True: 148 | input_data = json.dumps( 149 | { 150 | "inputText": query_value, 151 | "textGenerationConfig": model_params, 152 | } 153 | ) 154 | print(input_data) 155 | response = BEDROCK_CLIENT.invoke_model( 156 | body=input_data, modelId=MODEL_ID, accept=accept, contentType=contentType 157 | ) 158 | elif "claude-3" in MODEL_ID: 159 | body = json.dumps({ 160 | "anthropic_version": "bedrock-2023-05-31", 161 | "messages": messages, 162 | **model_params 163 | }) 164 | response = BEDROCK_CLIENT.invoke_model(body=body, modelId=MODEL_ID) 165 | else: 166 | body = json.dumps({"prompt": query_value, **model_params}) 167 | response = BEDROCK_CLIENT.invoke_model(body=body, modelId=MODEL_ID, accept=accept, contentType=contentType) 168 | 169 | response_body = json.loads(response.get("body").read()) 170 | 171 | if "amazon" in MODEL_ID: 172 | response = response_body.get("results")[0].get("outputText") 173 | elif "claude-3" in MODEL_ID: 174 | response = response_body.get("content")[0].get("text") 175 | elif "anthropic" in MODEL_ID: 176 | response = response_body.get("completion") 177 | else: 178 | LOGGER.info("Unknown model type!") 179 | print("Responese: ", response) 180 | 181 | return json.dumps(response) 182 | -------------------------------------------------------------------------------- /lab2/assets/lambda/db_connections/prompt_lambda/prompt_lambda.py: -------------------------------------------------------------------------------- 1 | """ 2 | Lambda that performs summarization with Bedrock 3 | """ 4 | 5 | ######################### 6 | # LIBRARIES & LOGGER 7 | ######################### 8 | import ast 9 | import json 10 | import logging 11 | import os 12 | import sys 13 | 14 | import boto3 15 | 16 | LOGGER = logging.Logger("DDB LAMBDA", level=logging.DEBUG) 17 | HANDLER = logging.StreamHandler(sys.stdout) 18 | HANDLER.setFormatter(logging.Formatter("%(levelname)s | %(name)s | %(message)s")) 19 | LOGGER.addHandler(HANDLER) 20 | 21 | 22 | def remove_dynamodb_type_descriptors(item): 23 | return {k: list(v.values())[0] for k, v in item.items()} 24 | 25 | 26 | ######################### 27 | # HANDLER 28 | ######################### 29 | 30 | 31 | def lambda_handler(event, context): 32 | """ 33 | Lambda handler 34 | """ 35 | LOGGER.info("Starting execution of lambda_handler()") 36 | 37 | ### PREPARATIONS 38 | # Convert the 'body' string to a dictionary 39 | LOGGER.info(f"The incoming payload event:{event}") 40 | 41 | body = json.loads(event["body"]) 42 | 43 | payload_type = body.get("type", {}) 44 | LOGGER.info(f"Payload type: {payload_type}") 45 | 46 | LOGGER.info(f"The incoming payload body:{body}") 47 | 48 | LOGGER.info("Standing up DDB connection!") 49 | dynamodb = boto3.client("dynamodb") 50 | 51 | # Get the DynamoDB table name from environment variable 52 | table_name = os.environ["TABLE_NAME"] 53 | LOGGER.info("boto3 dynamo established!") 54 | 55 | if payload_type == "PUT": 56 | # Extract the 'item' dictionary from the body 57 | item = body.get("item", {}) 58 | # Construct the item data as a dictionary 59 | item_data = { 60 | "session_id": {"S": item.get("session_id", "")}, 61 | "user_id": {"S": item.get("user_id", "")}, 62 | "timestamp": {"S": item.get("timestamp", "")}, 63 | "model": {"S": item.get("model", "")}, 64 | "answer_length": {"N": item.get("answer_length", "")}, 65 | "temperature": {"N": item.get("temperature", "")}, 66 | "Prompt Template": {"S": item.get("Prompt Template", "")}, 67 | "Prompt": {"S": item.get("Prompt", "")}, 68 | "Output": {"S": item.get("Output", "")}, 69 | } 70 | LOGGER.info(f"Item data: {item_data}") 71 | 72 | # Put the item into DynamoDB table 73 | try: 74 | response = dynamodb.put_item(TableName=table_name, Item=item_data) 75 | 76 | LOGGER.info(f"successfully put item!") 77 | return {"statusCode": 200, "body": json.dumps("Item successfully added to DynamoDB table")} 78 | except Exception as e: 79 | LOGGER.info(f"Unuccessful! Exception: {e}") 80 | return {"statusCode": 500, "body": json.dumps("Error adding item to DynamoDB table: {}".format(str(e)))} 81 | 82 | if payload_type == "GET": 83 | # Extract filter parameters 84 | filter_params = body.get("filter_params", {}) 85 | print(filter_params) 86 | user_ids_filter_str = filter_params.get("user_ids_filter", "") 87 | if user_ids_filter_str != "": 88 | user_ids_filter = ast.literal_eval(user_ids_filter_str) 89 | else: 90 | user_ids_filter = [] 91 | 92 | # retrieving filter values 93 | industry_filter = filter_params.get("industry_filter") 94 | language_filter = filter_params.get("language_filter") 95 | task_filter = filter_params.get("task_filter") 96 | technique_filter = filter_params.get("technique_filter") 97 | ai_model_filter = filter_params.get("ai_model_filter") 98 | 99 | user_id = filter_params.get("user_id") 100 | user_ids_filter.append(user_id) 101 | # Create FilterExpression and ExpressionAttributeValues 102 | filter_expressions = [] 103 | expression_attribute_values = {} 104 | # Use an expression attribute names dictionary to handle reserved words 105 | expression_attribute_names = {} 106 | 107 | # Incorporating user_ids_filter for possiblle filter expansion in UI 108 | if user_ids_filter: 109 | user_id_placeholders = [f":user_id_{i}" for i, _ in enumerate(user_ids_filter)] 110 | filter_expressions.append(f"user_id IN ({', '.join(user_id_placeholders)})") 111 | for placeholder, user_id in zip(user_id_placeholders, user_ids_filter): 112 | expression_attribute_values[placeholder] = {"S": user_id} 113 | 114 | if industry_filter and industry_filter != "ALL": 115 | filter_expressions.append("Industry = :industry") 116 | expression_attribute_values[":industry"] = {"S": industry_filter} 117 | 118 | # Incorporating language_filter 119 | if language_filter and language_filter != "ALL": 120 | filter_expressions.append("#Language = :language") 121 | expression_attribute_values[":language"] = {"S": language_filter} 122 | expression_attribute_names["#Language"] = "Language" 123 | 124 | # Incorporating task_filter 125 | if task_filter and task_filter != "ALL": 126 | filter_expressions.append("Task = :task") 127 | expression_attribute_values[":task"] = {"S": task_filter} 128 | 129 | # Incorporating technique_filter 130 | if technique_filter and technique_filter != "ALL": 131 | filter_expressions.append("Technique = :technique") 132 | expression_attribute_values[":technique"] = {"S": technique_filter} 133 | 134 | # Incorporating ai_model_filter 135 | if ai_model_filter and ai_model_filter != "ALL": 136 | filter_expressions.append("model = :model") 137 | expression_attribute_values[":model"] = {"S": ai_model_filter} 138 | 139 | filter_expression_string = " AND ".join(filter_expressions) 140 | print(f"FilterExpressions = {filter_expression_string}") 141 | print(f"Expression Attributes: {expression_attribute_values}") 142 | try: 143 | if expression_attribute_names != {}: 144 | response = dynamodb.scan( 145 | TableName=table_name, 146 | Limit=1000, 147 | FilterExpression=filter_expression_string if filter_expressions else None, 148 | ExpressionAttributeValues=expression_attribute_values if expression_attribute_values else None, 149 | ExpressionAttributeNames=expression_attribute_names if expression_attribute_names else None, 150 | ) 151 | else: 152 | # Use scan with FilterExpression to get items. Limit to 1000 items. 153 | response = dynamodb.scan( 154 | TableName=table_name, 155 | Limit=1000, 156 | FilterExpression=filter_expression_string if filter_expressions else None, 157 | ExpressionAttributeValues=expression_attribute_values if expression_attribute_values else None, 158 | ) 159 | 160 | items = response.get("Items", {}) 161 | LOGGER.info(f"Retrieved {len(items)} items") 162 | clean_item_list = [] 163 | for item in items: 164 | clean_item = remove_dynamodb_type_descriptors(item) 165 | clean_item_list.append(clean_item) 166 | 167 | # Return items in response 168 | return {"statusCode": 200, "body": json.dumps(clean_item_list)} 169 | except Exception as e: 170 | LOGGER.error(f"Error retrieving items: {e}") 171 | return {"statusCode": 500, "body": json.dumps(f"Error retrieving items from DynamoDB: {str(e)}")} 172 | 173 | elif payload_type == "DELETE": 174 | # Extract the 'session_id' from the body 175 | print("made it into the delete section! ") 176 | session_id = body["item"]["session_id"] 177 | LOGGER.debug(f"session_id {session_id}, {type(session_id)}") 178 | 179 | if not session_id: 180 | LOGGER.error("session_id is required for DELETE operation") 181 | return {"statusCode": 400, "body": json.dumps("session_id is required for DELETE operation")} 182 | 183 | try: 184 | response = dynamodb.delete_item( 185 | TableName=table_name, 186 | Key={"session_id": {"S": session_id}}, # "S" indicates that the datatype is a string. 187 | ) 188 | 189 | LOGGER.info(f"Deleted item with session_id: {session_id}") 190 | 191 | return {"statusCode": 200, "body": json.dumps(f"Item with session_id {session_id} successfully deleted")} 192 | except Exception as e: 193 | LOGGER.error(f"Error deleting item: {e}") 194 | return {"statusCode": 500, "body": json.dumps(f"Error deleting item from DynamoDB: {str(e)}")} 195 | 196 | else: 197 | return {"statusCode": 400, "body": json.dumps("Invalid payload type")} 198 | -------------------------------------------------------------------------------- /lab2/assets/streamlit/src/app_pages/CustomerList.py: -------------------------------------------------------------------------------- 1 | """ 2 | StreamLit app page: Prompt Catalog 3 | """ 4 | 5 | ######################### 6 | # IMPORTS & LOGGER 7 | ######################### 8 | 9 | import asyncio 10 | import datetime 11 | import json 12 | import logging 13 | import os 14 | import sys 15 | import re 16 | from pathlib import Path 17 | import pandas as pd 18 | from pandas.api.types import ( 19 | is_categorical_dtype, 20 | is_datetime64_any_dtype, 21 | is_numeric_dtype, 22 | is_object_dtype, 23 | ) 24 | from datetime import datetime 25 | 26 | 27 | import streamlit as st 28 | from st_pages import show_pages_from_config 29 | from streamlit_extras.switch_page_button import switch_page 30 | 31 | path = Path(os.path.dirname(__file__)) 32 | sys.path.append(str(path.parent.parent.absolute())) 33 | 34 | import components.authenticate as authenticate # noqa: E402 35 | import components.genai_api as genai_api # noqa: E402 36 | from components.utils import ( 37 | display_cover_with_title, 38 | reset_session_state, 39 | set_page_styling, 40 | add_logo, 41 | ) # noqa: E402 42 | from components.utils_models import get_models_specs # noqa: E402 43 | from components.utils_models import FILTER_BEDROCK_MODELS, BEDROCK_MODELS 44 | from components.utils import ( 45 | FILTER_INDUSTRIES, 46 | FILTER_TECHNIQUES, 47 | FILTER_LANGUAGES, 48 | FILTER_INDUSTRIES, 49 | FILTER_TASKS, 50 | ) 51 | 52 | LOGGER = logging.Logger("Prompt Catalog", level=logging.DEBUG) 53 | HANDLER = logging.StreamHandler(sys.stdout) 54 | HANDLER.setFormatter(logging.Formatter("%(levelname)s | %(name)s | %(message)s")) 55 | LOGGER.addHandler(HANDLER) 56 | 57 | 58 | ######################### 59 | # COVER & CONFIG 60 | ######################### 61 | 62 | # titles 63 | COVER_IMAGE = os.environ.get("COVER_IMAGE_URL") 64 | ASSISTANT_AVATAR = os.environ.get("ASSISTANT_AVATAR_URL") 65 | TITLE = "Customer List" 66 | DESCRIPTION = "Explore the data that comes from the source systems. " 67 | PAGE_TITLE = "Customer List" 68 | PAGE_ICON = ":book:" 69 | 70 | # page config 71 | st.set_page_config( 72 | page_title=PAGE_TITLE, 73 | page_icon=PAGE_ICON, 74 | layout="centered", 75 | initial_sidebar_state="expanded", 76 | ) 77 | 78 | # page width, form borders, message styling 79 | style_placeholder = st.empty() 80 | with style_placeholder: 81 | set_page_styling() 82 | 83 | # display cover immediately so that it does not pop in and out on every page refresh 84 | cover_placeholder = st.empty() 85 | with cover_placeholder: 86 | display_cover_with_title( 87 | title=TITLE, 88 | description=DESCRIPTION, 89 | image_url=COVER_IMAGE, 90 | max_width="100%", 91 | ) 92 | 93 | # custom page names in the sidebar 94 | show_pages_from_config() 95 | 96 | add_logo() 97 | 98 | ######################### 99 | # CHECK LOGIN (do not delete) 100 | ######################### 101 | 102 | # switch to home page if not authenticated 103 | authenticate.set_st_state_vars() 104 | if not st.session_state["authenticated"]: 105 | switch_page("Home") 106 | 107 | 108 | ######################### 109 | # CONSTANTS 110 | ######################### 111 | 112 | # page name for caching 113 | PAGE_NAME = "Customer List" 114 | 115 | ######################### 116 | # SESSION STATE VARIABLES 117 | ######################### 118 | 119 | reset_session_state(page_name=PAGE_NAME) 120 | st.session_state.setdefault("user_id", "AWS-User") 121 | st.session_state.setdefault("df", pd.DataFrame()) 122 | st.session_state.setdefault("df_selected", pd.DataFrame()) 123 | 124 | ######################### 125 | # HELPER FUNCTIONS 126 | ######################### 127 | 128 | 129 | def load_data_from_source_systems(): 130 | """ 131 | Loads the data from the source systems into the session state 132 | """ 133 | st.toast("Loading...") 134 | if "df" not in st.session_state or st.session_state["df"].shape[0] == 0: 135 | df = pd.read_csv(f"{path.parent.absolute()}/data/df_segment_data.csv") 136 | st.session_state["df"] = df 137 | st.session_state["df_name"] = "df_segment_data" 138 | else: 139 | df = st.session_state["df"] 140 | 141 | 142 | def extract_session_id_string(s): 143 | pattern = r"^\d+\s+(.+)$" 144 | match = re.match(pattern, s) 145 | if match: 146 | return match.group(1) 147 | else: 148 | return None 149 | 150 | 151 | def filter_dataframe(df: pd.DataFrame) -> pd.DataFrame: 152 | """ 153 | Adds a UI on top of a dataframe to let viewers filter columns 154 | Args: df (pd.DataFrame): Original dataframe 155 | Returns: pd.DataFrame: Filtered dataframe 156 | """ 157 | modify = st.checkbox("Add filters") 158 | if not modify: 159 | return df 160 | st.toast("Adding filters...") 161 | df = df.copy() 162 | # Try to convert datetimes into a standard format (datetime, no timezone) 163 | for col in df.columns: 164 | if is_object_dtype(df[col]): 165 | try: 166 | df[col] = pd.to_datetime(df[col]) 167 | except Exception as e: 168 | logger.exception(f"Error converting column {col} to datetime: {e}") 169 | if is_datetime64_any_dtype(df[col]): 170 | df[col] = df[col].dt.tz_localize(None) 171 | modification_container = st.container() 172 | with modification_container: 173 | to_filter_columns = st.multiselect("Filter dataframe on", df.columns) 174 | for column in to_filter_columns: 175 | left, right = st.columns((1, 20)) 176 | # Treat columns with < 10 unique values as categorical 177 | if is_categorical_dtype(df[column]) or df[column].nunique() < 10: 178 | user_cat_input = right.multiselect( 179 | f"Values for {column}", 180 | df[column].unique(), 181 | default=list(df[column].unique()), 182 | ) 183 | df = df[df[column].isin(user_cat_input)] 184 | elif is_numeric_dtype(df[column]): 185 | _min = float(df[column].min()) 186 | _max = float(df[column].max()) 187 | step = (_max - _min) / 100 188 | user_num_input = right.slider( 189 | f"Values for {column}", 190 | min_value=_min, 191 | max_value=_max, 192 | value=(_min, _max), 193 | step=step, 194 | ) 195 | df = df[df[column].between(*user_num_input)] 196 | elif is_datetime64_any_dtype(df[column]): 197 | user_date_input = right.date_input( 198 | f"Values for {column}", 199 | value=( 200 | df[column].min(), 201 | df[column].max(), 202 | ), 203 | ) 204 | if len(user_date_input) == 2: 205 | user_date_input = tuple(map(pd.to_datetime, user_date_input)) 206 | start_date, end_date = user_date_input 207 | df = df.loc[df[column].between(start_date, end_date)] 208 | else: 209 | user_text_input = right.text_input( 210 | f"Substring or regex in {column}", 211 | ) 212 | if user_text_input: 213 | df = df[df[column].astype(str).str.contains(user_text_input)] 214 | return df 215 | 216 | 217 | def dataframe_with_selections(df): 218 | df_with_selections = df.copy() 219 | df_with_selections.insert(0, "Select", False) 220 | 221 | # Get dataframe row-selections from user with st.data_editor 222 | edited_df = st.data_editor( 223 | df_with_selections, 224 | hide_index=True, 225 | column_config={"Select": st.column_config.CheckboxColumn(required=True)}, 226 | disabled=df.columns, 227 | ) 228 | 229 | # Filter the dataframe using the temporary column, then drop the column 230 | selected_rows = edited_df[edited_df.Select].drop("Select", axis=1) 231 | st.session_state["df_selected"] = selected_rows 232 | return selected_rows 233 | 234 | 235 | ######################### 236 | # MAIN APP PAGE 237 | ######################### 238 | st.text("") 239 | load_data_from_source_systems() 240 | 241 | if st.session_state["df"].shape[0] == 0: 242 | st.info("No data retrieved yet. Please click on 'Load Data'") 243 | _, col = st.columns([6, 1], gap="large") 244 | if col.button("Load data"): 245 | load_data_from_source_systems() 246 | else: 247 | df = st.session_state["df"] 248 | 249 | # instruction 250 | st.markdown("Select a user for Build Prompt Template experimentation.") 251 | 252 | selection = dataframe_with_selections(df) 253 | print(f"Selected user - {selection}") 254 | if not hasattr(st.session_state, "delete_prompt_clicked"): 255 | st.session_state.delete_prompt_clicked = False 256 | 257 | if len(selection) == 1: 258 | for name, content in selection.items(): 259 | if name == "User.UserId": 260 | user_id_customer = content[content.index[0]] 261 | 262 | st.markdown( 263 | f"Selected user: {selection['User.UserAttributes.FirstName'].iloc[0]} {selection['User.UserAttributes.LastName'].iloc[0]} ({selection['Address'].iloc[0]})" 264 | ) 265 | 266 | if st.button("Continue with selected user", type="primary"): 267 | st.session_state["df_selected"] = selection 268 | switch_page("Build Prompt Template") 269 | 270 | if len(selection) > 1: 271 | st.error("Please only select one user.") 272 | 273 | ######################### 274 | # FOOTNOTE 275 | ######################### 276 | 277 | # footnote 278 | st.text("") 279 | st.markdown("---") 280 | footer_col1, footer_col2 = st.columns(2) 281 | 282 | # log out button 283 | with footer_col1: 284 | if st.button("Sign out"): 285 | authenticate.sign_out() 286 | st.experimental_rerun() 287 | 288 | # copyright 289 | with footer_col2: 290 | st.markdown( 291 | "
© 2023 Amazon Web Services
", 292 | unsafe_allow_html=True, 293 | ) 294 | -------------------------------------------------------------------------------- /lab2/assets/streamlit/src/components/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper functions with StreamLit UI utils 3 | """ 4 | 5 | import os 6 | import json 7 | from pathlib import Path 8 | from datetime import datetime 9 | 10 | import qrcode 11 | import streamlit as st 12 | from qrcode.image.styledpil import StyledPilImage 13 | 14 | 15 | def generate_qrcode(url: str, path: str) -> str: 16 | """ 17 | Generate QR code for MFA 18 | 19 | Parameters 20 | ---------- 21 | url : str 22 | URL for the QR code 23 | path : str 24 | Folder to save generated codes 25 | 26 | Returns 27 | ------- 28 | str 29 | Local path to the QR code 30 | """ 31 | 32 | # create folder if needed 33 | if not os.path.exists(path): 34 | os.mkdir(path) 35 | 36 | # generate image 37 | qr = qrcode.QRCode( 38 | version=1, 39 | error_correction=qrcode.constants.ERROR_CORRECT_L, 40 | box_size=10, 41 | border=2, 42 | ) 43 | qr.add_data(url) 44 | qr.make(fit=True) 45 | img = qr.make_image(image_factory=StyledPilImage) 46 | 47 | # save locally 48 | current_ts = datetime.now().strftime("%d_%m_%Y_%H_%M_%S") 49 | qrcode_path = path + "qrcode_" + str(current_ts) + ".png" 50 | img.save(qrcode_path) 51 | return qrcode_path 52 | 53 | 54 | def set_page_styling() -> None: 55 | """ 56 | Set the page layout 57 | """ 58 | st.session_state[ 59 | "css_code" 60 | ] = """ 93 | """ 94 | st.markdown(st.session_state["css_code"], unsafe_allow_html=True) 95 | 96 | 97 | def generate_ai_summary_callback() -> None: 98 | """ 99 | Summary generation button callback 100 | """ 101 | st.session_state["generating_summary"] = True 102 | 103 | 104 | def display_search_result( 105 | documents: list, 106 | idx: int, 107 | ) -> None: 108 | """ 109 | Shows search result in the UI 110 | 111 | Parameters 112 | ---------- 113 | documents : list 114 | list of documents 115 | idx : int 116 | Index of the relevant document 117 | """ 118 | # extract document 119 | doc = documents[idx] 120 | 121 | # show document 122 | if "location_ref" in doc and "not found" not in doc["location_ref"]: 123 | st.markdown(f"[{doc['doc_name']}]({doc['doc_url']}) ({doc['location_ref']})") 124 | else: 125 | st.markdown(f"[{doc['doc_name']}]({doc['doc_url']})") 126 | st.markdown(doc["text_with_highlights"]) 127 | 128 | # show meta-data 129 | meta1_col, meta2_col, meta3_col = st.columns(3) 130 | with meta1_col: 131 | if "type" in doc: 132 | st.markdown( 133 | f"
Type: {doc['type'].lower()}
", 134 | unsafe_allow_html=True, 135 | ) 136 | with meta2_col: 137 | if "LANGUAGE" in doc: 138 | st.markdown( 139 | f"
Language: {doc['LANGUAGE'][0].lower()}
", 140 | unsafe_allow_html=True, 141 | ) 142 | with meta3_col: 143 | if "relevance" in doc: 144 | st.markdown( 145 | f"
Relevance: {doc['relevance'].lower().replace('_', ' ')}
", # noqa: E501 146 | unsafe_allow_html=True, 147 | ) 148 | 149 | 150 | def get_product_info(product_id, return_dict=True): 151 | path = Path(os.path.dirname(__file__)) 152 | with open(f"{path.parent.absolute()}/data/products.json", "r") as f: 153 | data = json.load(f) 154 | 155 | if return_dict: 156 | for product in data["products"]: 157 | if product["id"] == product_id: 158 | return product 159 | else: 160 | return data 161 | 162 | 163 | def display_product_info(card_info): 164 | # Extract the product name, title, and description 165 | product_name = card_info["Name"] 166 | product_title = card_info["Title"] 167 | product_description = card_info["Description"] 168 | 169 | # Extract the key features and great for sections 170 | key_features = card_info["Key Features"] 171 | great_for = card_info["Great For"] 172 | 173 | col1, col2 = st.columns([1, 1]) 174 | # Display the product name, title, and description 175 | with col1: 176 | st.write(f"**Product**:\n\n{product_name}") 177 | st.write(f"**Campaign Phrase**:\n\n{product_title}") 178 | # Display the key features and great for sections as lists 179 | with col2: 180 | st.write(f"**Description**:\n\n{product_description}") 181 | 182 | col1, col2 = st.columns([1, 1]) 183 | with col1: 184 | st.write("**Key Features**:") 185 | for feature in key_features: 186 | st.write("- " + feature) 187 | with col2: 188 | st.write("**Great For**:") 189 | for use_case in great_for: 190 | st.write("- " + use_case) 191 | 192 | 193 | def reset_session_state(page_name: str) -> None: 194 | """ 195 | Resets session state variables 196 | """ 197 | st.session_state.setdefault("last_page", "None") 198 | 199 | st.session_state["current_page"] = page_name 200 | if st.session_state["current_page"] != st.session_state["last_page"]: 201 | for key in st.session_state.keys(): 202 | if key not in [ 203 | "authenticated", 204 | "access_token", 205 | "css_code", 206 | "username", 207 | "user_id", 208 | "user_email", 209 | "email", 210 | "df", 211 | "df_name", 212 | "df_selected", 213 | "df_selected_prompt", 214 | ]: 215 | del st.session_state[key] 216 | 217 | st.session_state["last_page"] = page_name 218 | 219 | 220 | def button_with_url( 221 | url: str, 222 | text: str, 223 | ) -> str: 224 | """ 225 | Create button with URL link 226 | """ 227 | return f""" 228 | 239 | """ 240 | 241 | 242 | def display_cover_with_title( 243 | title: str, 244 | description: str, 245 | image_url: str, 246 | max_width: str = "100%", 247 | text_color: str = "#FFFFFF", 248 | ) -> None: 249 | """ 250 | Display cover with title 251 | 252 | Parameters 253 | ---------- 254 | title : str 255 | Title to display over the image (upper part) 256 | description : str 257 | Description to display over the image (lower part) 258 | image_url : str 259 | URL to the cover image 260 | """ 261 | 262 | html_code = f""" 263 |
264 | Cover 265 |
{title}
266 |
{description}
267 |
268 | """ # noqa: E501 269 | 270 | st.markdown( 271 | html_code, 272 | unsafe_allow_html=True, 273 | ) 274 | 275 | 276 | INDUSTRIES = [ 277 | "Automotive", 278 | "FinServ", 279 | "Gaming", 280 | "HCLS", 281 | "Hospitality", 282 | "M&E", 283 | "MFG", 284 | "P&U", 285 | "PS (Education, Government, etc.)", 286 | "R/CPG", 287 | "Telco", 288 | "Transport", 289 | "Travel", 290 | ] 291 | 292 | LANGUAGES = [ 293 | "English", 294 | "German", 295 | "French", 296 | "Italian", 297 | "Spanish", 298 | "Polish", 299 | "Romanian", 300 | "Dutch", 301 | "Russian", 302 | "Portuguese", 303 | ] 304 | 305 | TECHNIQUES = [ 306 | "Zero-shot", 307 | "Few-shot", 308 | "Chain of Thoughts (CoT)", 309 | "Reasoning Acting (ReAct)", 310 | "Other", 311 | ] 312 | 313 | TASKS = [ 314 | "Classification", 315 | "Code Gen or refactoring", 316 | "Summarization", 317 | "Q&A", 318 | "Chatbots", 319 | "Other", 320 | ] 321 | 322 | 323 | FILTER_INDUSTRIES = ["ALL"] + INDUSTRIES 324 | 325 | FILTER_LANGUAGES = ["ALL"] + LANGUAGES 326 | 327 | FILTER_TECHNIQUES = ["ALL"] + TECHNIQUES 328 | 329 | FILTER_TASKS = ["ALL"] + TASKS 330 | 331 | 332 | ######################### 333 | # LOGO Placement & Title 334 | ######################### 335 | 336 | 337 | def add_logo(): 338 | st.markdown( 339 | """ 340 | 355 | """, 356 | unsafe_allow_html=True, 357 | ) 358 | -------------------------------------------------------------------------------- /lab2/infra/stacks/bdrk_reinvent_streamlit.py: -------------------------------------------------------------------------------- 1 | """ 2 | bdrk_reinvent Streamlit stack 3 | """ 4 | 5 | import json 6 | import os 7 | from pathlib import Path 8 | 9 | from aws_cdk import CfnOutput as output 10 | from aws_cdk import NestedStack, Tags 11 | from aws_cdk import aws_ec2 as ec2 12 | from aws_cdk import aws_ecs as ecs 13 | from aws_cdk import aws_elasticloadbalancingv2 as elbv2 14 | from aws_cdk import aws_s3 as _s3 15 | from aws_cdk.aws_ecr_assets import DockerImageAsset 16 | from constructs import Construct 17 | from aws_cdk import aws_cloudfront as cloudfront 18 | from aws_cdk.aws_wafv2 import CfnWebACL 19 | from aws_cdk.aws_cloudfront_origins import LoadBalancerV2Origin 20 | import aws_cdk as cdk 21 | 22 | 23 | class bdrk_reinventStreamlitStack(NestedStack): 24 | def __init__( 25 | self, 26 | scope: Construct, 27 | id: str, 28 | stack_name: str, 29 | ecs_cpu: int = 512, 30 | ecs_memory: int = 1024, 31 | client_id: str = None, 32 | api_uri: str = None, 33 | cover_image_url: str = None, 34 | cover_image_login_url: str = None, 35 | assistant_avatar: str = "assistant", 36 | open_to_public_internet=False, 37 | ip_address_allowed: list = None, 38 | retriever_options: list = None, 39 | sm_endpoints: dict = None, 40 | custom_header_name="X-Custom-Header", 41 | custom_header_value="MyNewCustomHeaderValue", 42 | **kwargs, 43 | ) -> None: 44 | super().__init__(scope, id, **kwargs) 45 | 46 | self.prefix = stack_name 47 | self.ecs_cpu = ecs_cpu 48 | self.ecs_memory = ecs_memory 49 | self.client_id = client_id 50 | self.api_uri = api_uri 51 | self.cover_image_url = cover_image_url 52 | self.cover_image_login_url = cover_image_login_url 53 | self.assistant_avatar = assistant_avatar 54 | self.ip_address_allowed = ip_address_allowed 55 | self.custom_header_name = custom_header_name 56 | self.custom_header_value = custom_header_value 57 | 58 | self.retriever_options = retriever_options if retriever_options is not None else ["N/A"] 59 | 60 | self.docker_asset = self.build_docker_push_ecr() 61 | self.vpc = self.create_webapp_vpc(open_to_public_internet=open_to_public_internet) 62 | 63 | self.cluster, self.alb, self.cloudfront = self.create_ecs_and_alb( 64 | open_to_public_internet=open_to_public_internet 65 | ) 66 | 67 | # Add to hosted UI in Cognito Console: 68 | # https://bdrk_reinvent.click 69 | # https://bdrk_reinvent.click/oauth2/idpresponse 70 | 71 | self.alb_dns_name = output(self, id="AlbDnsName", value=self.alb.load_balancer_dns_name) 72 | self.cloudfront_distribution_name = output( 73 | self, id="cloudfront_distribution_name", value=self.cloudfront.domain_name 74 | ) 75 | ## **************** Tags **************** 76 | Tags.of(self).add("StackName", id) 77 | Tags.of(self).add("Team", "Bedrock Workshop") 78 | 79 | def build_docker_push_ecr(self): 80 | # ECR: Docker build and push to ECR 81 | return DockerImageAsset( 82 | self, 83 | "StreamlitImg", 84 | # asset_name = f"{prefix}-streamlit-img", 85 | directory=os.path.join(Path(__file__).parent.parent.parent, "assets/streamlit"), 86 | ) 87 | 88 | def create_webapp_vpc(self, open_to_public_internet=False): 89 | # VPC for ALB and ECS cluster 90 | vpc = ec2.Vpc( 91 | self, 92 | "WebappVpc", 93 | ip_addresses=ec2.IpAddresses.cidr("10.0.0.0/16"), 94 | max_azs=2, 95 | vpc_name=f"{self.prefix}-stl-vpc", 96 | nat_gateways=1, 97 | ) 98 | 99 | self.ecs_security_group = ec2.SecurityGroup( 100 | self, 101 | "SecurityGroupECS", 102 | vpc=vpc, 103 | security_group_name=f"{self.prefix}-stl-ecs-sg", 104 | ) 105 | self.ecs_security_group.add_ingress_rule( 106 | peer=self.ecs_security_group, 107 | connection=ec2.Port.all_traffic(), 108 | description="Within Security Group", 109 | ) 110 | 111 | self.alb_security_group = ec2.SecurityGroup( 112 | self, 113 | "SecurityGroupALB", 114 | vpc=vpc, 115 | security_group_name=f"{self.prefix}-stl-alb-sg", 116 | ) 117 | self.alb_security_group.add_ingress_rule( 118 | peer=self.alb_security_group, 119 | connection=ec2.Port.all_traffic(), 120 | description="Within Security Group", 121 | ) 122 | 123 | if self.ip_address_allowed: 124 | for ip in self.ip_address_allowed: 125 | if ip.startswith("pl-"): 126 | _peer = ec2.Peer.prefix_list(ip) 127 | # cf https://apll.tools.aws.dev/#/ 128 | else: 129 | _peer = ec2.Peer.ipv4(ip) 130 | # cf https://dogfish.amazon.com/#/search?q=Unfabric&attr.scope=PublicIP 131 | self.alb_security_group.add_ingress_rule( 132 | peer=_peer, 133 | connection=ec2.Port.tcp(80), 134 | ) 135 | 136 | # Change IP address to developer IP for testing 137 | # self.alb_security_group.add_ingress_rule(peer=ec2.Peer.ipv4("1.2.3.4/32"), 138 | # connection=ec2.Port.tcp(443), description = "Developer IP") 139 | 140 | self.ecs_security_group.add_ingress_rule( 141 | peer=self.alb_security_group, 142 | connection=ec2.Port.tcp(8501), 143 | description="ALB traffic", 144 | ) 145 | 146 | return vpc 147 | 148 | def create_ecs_and_alb(self, open_to_public_internet=False): 149 | # ECS cluster and service definition 150 | 151 | cluster = ecs.Cluster(self, "Cluster", enable_fargate_capacity_providers=True, vpc=self.vpc) 152 | 153 | alb_suffix = "" if open_to_public_internet else "-priv" 154 | 155 | # ALB to connect to ECS 156 | alb = elbv2.ApplicationLoadBalancer( 157 | self, 158 | f"{self.prefix}-alb{alb_suffix}", 159 | vpc=self.vpc, 160 | internet_facing=open_to_public_internet, 161 | load_balancer_name=f"{self.prefix}-stl{alb_suffix}", 162 | security_group=self.alb_security_group, 163 | vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PUBLIC), 164 | ) 165 | 166 | fargate_task_definition = ecs.FargateTaskDefinition( 167 | self, 168 | "WebappTaskDef", 169 | memory_limit_mib=self.ecs_memory, 170 | cpu=self.ecs_cpu, 171 | ) 172 | 173 | # app_uri = f"http://{alb.load_balancer_dns_name}" 174 | 175 | fargate_task_definition.add_container( 176 | "WebContainer", 177 | # Use an image from DockerHub 178 | image=ecs.ContainerImage.from_docker_image_asset(self.docker_asset), 179 | port_mappings=[ecs.PortMapping(container_port=8501, protocol=ecs.Protocol.TCP)], 180 | environment={ # "COGNITO_DOMAIN" : self.cognito_domain.domain_name, 181 | "CLIENT_ID": self.client_id, 182 | "API_URI": self.api_uri, 183 | "COVER_IMAGE_URL": self.cover_image_url, 184 | "COVER_IMAGE_LOGIN_URL": self.cover_image_login_url, 185 | }, 186 | logging=ecs.LogDrivers.aws_logs(stream_prefix="WebContainerLogs"), 187 | ) 188 | 189 | service = ecs.FargateService( 190 | self, 191 | "StreamlitECSService", 192 | cluster=cluster, 193 | task_definition=fargate_task_definition, 194 | service_name=f"{self.prefix}-stl-front", 195 | security_groups=[self.ecs_security_group], 196 | vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS), 197 | ) 198 | 199 | """' 200 | Code for https traffic with certificate 201 | 202 | ecs_target_group = alb.ApplicationTargetGroup( 203 | self, 204 | "ecs-target-group", 205 | port=8501, 206 | protocol=elbv2.ApplicationProtocol.HTTP, 207 | targets=[service], 208 | ) 209 | 210 | https_listener = alb.add_listener( 211 | "Listener", 212 | port=443, 213 | certificates=[certificate] 214 | ), 215 | ) 216 | 217 | http_redirect = alb.add_redirect( 218 | source_port=80, 219 | source_protocol=elbv2.ApplicationProtocol.HTTP, 220 | target_port=443, 221 | target_protocol=elbv2.ApplicationProtocol.HTTPS, 222 | ) 223 | """ 224 | 225 | # ********* Cloudfront distribution ********* 226 | 227 | # Add ALB as CloudFront Origin 228 | origin = LoadBalancerV2Origin( 229 | alb, 230 | custom_headers={self.custom_header_name: self.custom_header_value}, 231 | origin_shield_enabled=False, 232 | protocol_policy=cloudfront.OriginProtocolPolicy.HTTP_ONLY, 233 | ) 234 | 235 | cloudfront_distribution = cloudfront.Distribution( 236 | self, 237 | f"{self.prefix}-cf-dist", 238 | default_behavior=cloudfront.BehaviorOptions( 239 | origin=origin, 240 | viewer_protocol_policy=cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, 241 | allowed_methods=cloudfront.AllowedMethods.ALLOW_ALL, 242 | cache_policy=cloudfront.CachePolicy.CACHING_DISABLED, 243 | origin_request_policy=cloudfront.OriginRequestPolicy.ALL_VIEWER, 244 | ), 245 | minimum_protocol_version=cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021, 246 | ) 247 | 248 | # ********* ALB Listener ********* 249 | http_listener = alb.add_listener( 250 | f"{self.prefix}-http-listener{alb_suffix}", 251 | port=80, 252 | open=not (bool(self.ip_address_allowed)), 253 | ) 254 | 255 | http_listener.add_targets( 256 | f"{self.prefix}-tg{alb_suffix}", 257 | target_group_name=f"{self.prefix}-tg{alb_suffix}", 258 | port=8501, 259 | priority=1, 260 | conditions=[elbv2.ListenerCondition.http_header(self.custom_header_name, [self.custom_header_value])], 261 | protocol=elbv2.ApplicationProtocol.HTTP, 262 | targets=[service], 263 | ) 264 | # add a default action to the listener that will deny all requests that do not have the custom header 265 | http_listener.add_action( 266 | "default-action", 267 | action=elbv2.ListenerAction.fixed_response( 268 | status_code=403, 269 | content_type="text/plain", 270 | message_body="Access denied", 271 | ), 272 | ) 273 | 274 | return cluster, alb, cloudfront_distribution 275 | -------------------------------------------------------------------------------- /lab2/assets/streamlit/src/app_pages/PromptCatalog.py: -------------------------------------------------------------------------------- 1 | """ 2 | StreamLit app page: Prompt Catalog 3 | """ 4 | 5 | ######################### 6 | # IMPORTS & LOGGER 7 | ######################### 8 | 9 | import logging 10 | import os 11 | import sys 12 | import re 13 | from pathlib import Path 14 | import pandas as pd 15 | from pandas.api.types import ( 16 | is_categorical_dtype, 17 | is_datetime64_any_dtype, 18 | is_numeric_dtype, 19 | is_object_dtype, 20 | ) 21 | 22 | import streamlit as st 23 | from st_pages import show_pages_from_config 24 | from streamlit_extras.switch_page_button import switch_page 25 | 26 | path = Path(os.path.dirname(__file__)) 27 | sys.path.append(str(path.parent.parent.absolute())) 28 | 29 | import components.authenticate as authenticate # noqa: E402 30 | import components.genai_api as genai_api # noqa: E402 31 | from components.utils import ( 32 | display_cover_with_title, 33 | reset_session_state, 34 | set_page_styling, 35 | add_logo, 36 | ) # noqa: E402 37 | from components.utils_models import get_models_specs # noqa: E402 38 | from components.utils_models import FILTER_BEDROCK_MODELS, BEDROCK_MODELS 39 | from components.utils import ( 40 | FILTER_INDUSTRIES, 41 | FILTER_TECHNIQUES, 42 | FILTER_LANGUAGES, 43 | FILTER_INDUSTRIES, 44 | FILTER_TASKS, 45 | ) 46 | 47 | LOGGER = logging.Logger("Prompt Catalog", level=logging.DEBUG) 48 | HANDLER = logging.StreamHandler(sys.stdout) 49 | HANDLER.setFormatter(logging.Formatter("%(levelname)s | %(name)s | %(message)s")) 50 | LOGGER.addHandler(HANDLER) 51 | 52 | 53 | ######################### 54 | # COVER & CONFIG 55 | ######################### 56 | 57 | # titles 58 | COVER_IMAGE = os.environ.get("COVER_IMAGE_URL") 59 | TITLE = "Prompt Catalog" 60 | DESCRIPTION = "Explore your Prompt Catalog." 61 | PAGE_TITLE = "Prompt Catalog" 62 | PAGE_ICON = ":book:" 63 | 64 | # page config 65 | st.set_page_config( 66 | page_title=PAGE_TITLE, 67 | page_icon=PAGE_ICON, 68 | layout="centered", 69 | initial_sidebar_state="expanded", 70 | ) 71 | 72 | # page width, form borders, message styling 73 | style_placeholder = st.empty() 74 | with style_placeholder: 75 | set_page_styling() 76 | 77 | # display cover immediately so that it does not pop in and out on every page refresh 78 | cover_placeholder = st.empty() 79 | with cover_placeholder: 80 | display_cover_with_title( 81 | title=TITLE, 82 | description=DESCRIPTION, 83 | image_url=COVER_IMAGE, 84 | max_width="100%", 85 | ) 86 | 87 | # custom page names in the sidebar 88 | show_pages_from_config() 89 | 90 | add_logo() 91 | 92 | ######################### 93 | # CHECK LOGIN (do not delete) 94 | ######################### 95 | 96 | # switch to home page if not authenticated 97 | authenticate.set_st_state_vars() 98 | if not st.session_state["authenticated"]: 99 | switch_page("Home") 100 | 101 | 102 | ######################### 103 | # CONSTANTS 104 | ######################### 105 | 106 | # page name for caching 107 | PAGE_NAME = "PromptCatalog" 108 | 109 | ######################### 110 | # SESSION STATE VARIABLES 111 | ######################### 112 | 113 | reset_session_state(page_name=PAGE_NAME) 114 | 115 | MODELS_DISPLAYED, _ = get_models_specs(path=path) 116 | st.session_state.setdefault("ai_model", MODELS_DISPLAYED[0]) # default model 117 | 118 | # if "ai_model" not in st.session_state: 119 | # st.session_state["ai_model"] = MODELS_DISPLAYED[0] # default model 120 | LOGGER.debug("ai_model selected: %s", st.session_state["ai_model"]) 121 | 122 | st.session_state.setdefault("query", "") 123 | st.session_state.setdefault("user_id", "AWS-User") 124 | st.session_state.setdefault("prompt_df", pd.DataFrame()) 125 | 126 | ######################### 127 | # HELPER FUNCTIONS 128 | ######################### 129 | 130 | 131 | def extract_session_id_string(s): 132 | pattern = r"^\d+\s+(.+)$" 133 | match = re.match(pattern, s) 134 | if match: 135 | return match.group(1) 136 | else: 137 | return None 138 | 139 | 140 | def get_all_user_prompts() -> None: 141 | """ 142 | Runs API call to retrieve LLM answer and references 143 | """ 144 | user_id = st.session_state["user_id"] 145 | 146 | ai_model_session = st.session_state.get("ai_model", "ALL") 147 | 148 | LOGGER.info("Retrieving all prompts for user: %s", user_id) 149 | 150 | with st.spinner("Retrieving prompts..."): 151 | response = genai_api.invoke_dynamo_get( 152 | params={ 153 | "user_id": user_id, 154 | "ai_model_filter": ai_model_session, 155 | }, 156 | access_token=st.session_state["access_token"], 157 | ) 158 | 159 | prompt_dataframe = pd.DataFrame(response.json()) 160 | st.session_state["prompt_df"] = prompt_dataframe 161 | 162 | 163 | def delete_prompt(prompt_id: str) -> None: 164 | """ 165 | Runs API call to retrieve LLM answer and references 166 | """ 167 | LOGGER.debug("Deleting prompt: ####%s#####", prompt_id) 168 | with st.spinner("Generating content..."): 169 | response = genai_api.invoke_dynamo_delete( 170 | params={"session_id": prompt_id}, 171 | access_token=st.session_state["access_token"], 172 | ) 173 | 174 | if response.status_code == 200: 175 | st.success(f"Prompt {prompt_id} deleted successfully") 176 | prompt_df = st.session_state["prompt_df"] 177 | prompt_df.drop( 178 | prompt_df[prompt_df["session_id"] == prompt_id].index, inplace=True 179 | ) 180 | st.session_state["prompt_df"] = prompt_df 181 | else: 182 | st.error(f"Prompt {prompt_id} deletion failed") 183 | 184 | 185 | def filter_dataframe(df: pd.DataFrame) -> pd.DataFrame: 186 | """ 187 | Adds a UI on top of a dataframe to let viewers filter columns 188 | Args: df (pd.DataFrame): Original dataframe 189 | Returns: pd.DataFrame: Filtered dataframe 190 | """ 191 | modify = st.checkbox("Add filters") 192 | if not modify: 193 | return df 194 | df = df.copy() 195 | # Try to convert datetimes into a standard format (datetime, no timezone) 196 | for col in df.columns: 197 | if is_object_dtype(df[col]): 198 | try: 199 | df[col] = pd.to_datetime(df[col]) 200 | except Exception as e: 201 | LOGGER.exception("Error converting column %s to datetime: %s", col, e) 202 | if is_datetime64_any_dtype(df[col]): 203 | df[col] = df[col].dt.tz_localize(None) 204 | modification_container = st.container() 205 | with modification_container: 206 | to_filter_columns = st.multiselect("Filter dataframe on", df.columns) 207 | for column in to_filter_columns: 208 | left, right = st.columns((1, 20)) 209 | # Treat columns with < 10 unique values as categorical 210 | if is_categorical_dtype(df[column]) or df[column].nunique() < 10: 211 | user_cat_input = right.multiselect( 212 | f"Values for {column}", 213 | df[column].unique(), 214 | default=list(df[column].unique()), 215 | ) 216 | df = df[df[column].isin(user_cat_input)] 217 | elif is_numeric_dtype(df[column]): 218 | _min = float(df[column].min()) 219 | _max = float(df[column].max()) 220 | step = (_max - _min) / 100 221 | user_num_input = right.slider( 222 | f"Values for {column}", 223 | min_value=_min, 224 | max_value=_max, 225 | value=(_min, _max), 226 | step=step, 227 | ) 228 | df = df[df[column].between(*user_num_input)] 229 | elif is_datetime64_any_dtype(df[column]): 230 | user_date_input = right.date_input( 231 | f"Values for {column}", 232 | value=( 233 | df[column].min(), 234 | df[column].max(), 235 | ), 236 | ) 237 | if len(user_date_input) == 2: 238 | user_date_input = tuple(map(pd.to_datetime, user_date_input)) 239 | start_date, end_date = user_date_input 240 | df = df.loc[df[column].between(start_date, end_date)] 241 | else: 242 | user_text_input = right.text_input( 243 | f"Substring or regex in {column}", 244 | ) 245 | if user_text_input: 246 | df = df[df[column].astype(str).str.contains(user_text_input)] 247 | return df 248 | 249 | 250 | def dataframe_with_selections(df): 251 | df_with_selections = df.copy() 252 | df_with_selections.insert(0, "Select", False) 253 | 254 | # Get dataframe row-selections from user with st.data_editor 255 | edited_df = st.data_editor( 256 | df_with_selections, 257 | hide_index=True, 258 | column_config={"Select": st.column_config.CheckboxColumn(required=True)}, 259 | disabled=df.columns, 260 | ) 261 | 262 | # Filter the dataframe using the temporary column, then drop the column 263 | selected_rows = edited_df[edited_df.Select] 264 | return selected_rows.drop("Select", axis=1) 265 | 266 | 267 | ######################### 268 | # SIDEBAR 269 | ######################### 270 | 271 | # sidebar 272 | with st.sidebar: 273 | MODEL_OPTIONS = ["ALL"] + MODELS_DISPLAYED 274 | ai_model = st.selectbox( 275 | label="Language model:", 276 | options=MODEL_OPTIONS, 277 | format_func=lambda x: "Sagemaker: " + x 278 | if not x.startswith("Bedrock:") and x != "ALL" 279 | else x, 280 | key="ai_model", 281 | help="The search app provides flexibility to choose a large language model used for AI answers.", 282 | ) 283 | st.session_state["ai_model_filter"] = ai_model 284 | 285 | ######################### 286 | # MAIN APP PAGE 287 | ######################### 288 | st.text("") 289 | get_all_user_prompts() 290 | if st.session_state["prompt_df"].shape[0] == 0: 291 | st.info("No prompts for this model. Select your model on the left bar.") 292 | get_all_user_prompts() 293 | else: 294 | st.markdown("Select a prompt template from your prompt calalog.") 295 | prompt_df = st.session_state["prompt_df"] 296 | selection = dataframe_with_selections(prompt_df) 297 | if len(selection) == 1: 298 | if st.button("Continue with selected prompt template", type="primary"): 299 | st.session_state[ 300 | "df_selected_prompt" 301 | ] = selection # Todo - add it to session state 302 | switch_page("email generation wizard") 303 | st.markdown("## Details") 304 | prompt_content = None 305 | output_content = None 306 | prompt_template_content = None 307 | 308 | for name, content in selection.items(): 309 | if name == "session_id": 310 | session_id_string = content[content.index[0]] 311 | elif name == "user_id": 312 | user_id_string_prompt_catalog = content[content.index[0]] 313 | elif name == "Prompt Template": 314 | prompt_template_content = content[content.index[0]] 315 | elif name == "Prompt": 316 | prompt_content = content[content.index[0]] 317 | elif name == "Output": 318 | output_content = content[content.index[0]] 319 | else: 320 | col1, col2 = st.columns(2) 321 | with col1: 322 | st.markdown(f"**{name}:**") 323 | with col2: 324 | st.markdown(content[content.index[0]]) 325 | if prompt_template_content: 326 | st.markdown("**Prompt Template:**") 327 | st.markdown(prompt_template_content) 328 | 329 | if prompt_content: 330 | st.markdown("**Prompt:**") 331 | st.markdown(prompt_content) 332 | 333 | if output_content: 334 | st.markdown("**Output:**") 335 | st.markdown(output_content) 336 | 337 | 338 | ######################### 339 | # FOOTNOTE 340 | ######################### 341 | 342 | # footnote 343 | st.markdown("---") 344 | footer_col1, footer_col2 = st.columns(2) 345 | 346 | # log out button 347 | with footer_col1: 348 | if st.button("Sign out"): 349 | authenticate.sign_out() 350 | st.experimental_rerun() 351 | 352 | # copyright 353 | with footer_col2: 354 | st.markdown( 355 | "
© 2023 Amazon Web Services
", 356 | unsafe_allow_html=True, 357 | ) 358 | -------------------------------------------------------------------------------- /lab2/assets/streamlit/src/app_pages/BuildPromptTemplate.py: -------------------------------------------------------------------------------- 1 | """ 2 | Build Prompt Template Page 3 | """ 4 | 5 | ######################### 6 | # IMPORTS & LOGGER 7 | ######################### 8 | import logging 9 | import os 10 | import sys 11 | from pathlib import Path 12 | import boto3 13 | import re 14 | 15 | from datetime import datetime 16 | 17 | # import dynamodb_json as djson 18 | 19 | import streamlit as st 20 | from st_pages import show_pages_from_config 21 | from streamlit_extras.switch_page_button import switch_page 22 | 23 | path = Path(os.path.dirname(__file__)) 24 | sys.path.append(str(path.parent.parent.parent.absolute())) 25 | 26 | import components.authenticate as authenticate # noqa: E402 27 | import components.genai_api as genai_api # noqa: E402 28 | from components.utils import ( 29 | display_cover_with_title, 30 | reset_session_state, 31 | set_page_styling, 32 | get_product_info, 33 | display_product_info, 34 | ) # noqa: E402 35 | from components.utils_models import get_models_specs # noqa: E402 36 | from components.utils_models import BEDROCK_MODELS 37 | from components.utils import TASKS, TECHNIQUES, LANGUAGES, INDUSTRIES, add_logo 38 | 39 | LOGGER = logging.Logger("Q&A", level=logging.DEBUG) 40 | HANDLER = logging.StreamHandler(sys.stdout) 41 | HANDLER.setFormatter(logging.Formatter("%(levelname)s | %(name)s | %(message)s")) 42 | LOGGER.addHandler(HANDLER) 43 | 44 | 45 | ######################### 46 | # COVER & CONFIG 47 | ######################### 48 | 49 | 50 | # titles 51 | COVER_IMAGE = os.environ.get("COVER_IMAGE_URL") 52 | TITLE = "Build Prompt Template" 53 | DESCRIPTION = "Build your own prompt template to generate marketing emails." 54 | PAGE_TITLE = "Build Prompt Template" 55 | PAGE_ICON = ":robot:" 56 | REGION = os.environ.get("REGION") 57 | # page config 58 | st.set_page_config( 59 | page_title=PAGE_TITLE, 60 | page_icon=PAGE_ICON, 61 | layout="centered", 62 | initial_sidebar_state="expanded", 63 | ) 64 | 65 | # page width, form borders, message styling 66 | style_placeholder = st.empty() 67 | with style_placeholder: 68 | set_page_styling() 69 | 70 | # display cover immediately so that it does not pop in and out on every page refresh 71 | cover_placeholder = st.empty() 72 | with cover_placeholder: 73 | display_cover_with_title( 74 | title=TITLE, 75 | description=DESCRIPTION, 76 | image_url=COVER_IMAGE, 77 | max_width="100%", 78 | ) 79 | 80 | # custom page names in the sidebar 81 | show_pages_from_config() 82 | 83 | add_logo() 84 | 85 | 86 | ######################### 87 | # CHECK LOGIN (do not delete) 88 | ######################### 89 | 90 | # switch to home page if not authenticated 91 | authenticate.set_st_state_vars() 92 | if not st.session_state["authenticated"]: 93 | switch_page("Home") 94 | 95 | 96 | ######################### 97 | # CONSTANTS 98 | ######################### 99 | 100 | # page name for caching 101 | PAGE_NAME = "run_prompt" 102 | 103 | SM_ENDPOINTS = {} # json.loads(os.environ.get("SM_ENDPOINTS")) 104 | MODELS_DISPLAYED, MODEL_SPECS = get_models_specs(path=path) 105 | # MODELS_DISPLAYED, MODEL_SPECS = get_models_specs({}, path=path) 106 | 107 | ######################### 108 | # SESSION STATE VARIABLES 109 | ######################### 110 | 111 | reset_session_state(page_name=PAGE_NAME) 112 | 113 | st.session_state.setdefault("ai_model", MODELS_DISPLAYED[0]) # default model 114 | LOGGER.log(logging.DEBUG, (f"ai_model selected: {st.session_state['ai_model']}")) 115 | 116 | st.session_state.setdefault("query", "") 117 | 118 | 119 | ######################### 120 | # HELPER FUNCTIONS 121 | ######################### 122 | def generate_session_id(user_id: str = "AWS") -> None: 123 | """ 124 | Generates unique chat id based on user name and timestamp 125 | """ 126 | timestamp = datetime.now().strftime("%Y-%m-%d-%H-%M-%S") 127 | session_id = f"{user_id}-{timestamp}" 128 | st.session_state["session_id"] = session_id 129 | LOGGER.log(logging.DEBUG, (f"session_id: {session_id}")) 130 | return session_id, timestamp 131 | 132 | 133 | def format_prompt(prompt: str) -> str: 134 | """ 135 | Replaces input parameter in prompt string with actual user values and product information. 136 | """ 137 | LOGGER.debug("INSIDE format_prompt()") 138 | # Remove dots inside curly braces but keep dots outside. 139 | prompt_cleaned = re.sub(r"\{(.*?)\}", lambda x: x.group(0).replace(".", ""), prompt) 140 | LOGGER.debug(f"PROMPT TEMPLATE: {prompt_cleaned}") 141 | # Convert DataFrame to Series to Dictionary 142 | user_attributes_dict = st.session_state["df_selected"].squeeze().to_dict() 143 | # Remove dots inside dictionary keys 144 | user_attributes_cleaned = { 145 | key.replace(".", ""): value for key, value in user_attributes_dict.items() 146 | } 147 | product_info_cleaned = { 148 | key.replace("Name", "Product"): value 149 | for key, value in st.session_state["product_info"].items() 150 | } 151 | product_info_cleaned = { 152 | key.replace("Title", "Campaign Phrase"): value 153 | for key, value in product_info_cleaned.items() 154 | } 155 | 156 | prompt_formatted = "" 157 | try: 158 | prompt_formatted = prompt_cleaned.format( 159 | **user_attributes_cleaned, **product_info_cleaned 160 | ) 161 | except KeyError as e: 162 | LOGGER.error(f"Invalid input parameter in prompt: {e}") 163 | st.error(f"Invalid input parameter in prompt: {e}") 164 | LOGGER.debug(f"PROMPT FORMATTED: {prompt_formatted}") 165 | return prompt_formatted 166 | 167 | 168 | def run_genai_prompt(ai_model, prompt, answer_length=4096, temperature=0) -> str: 169 | """ 170 | Runs API call to retrieve LLM answer and references 171 | """ 172 | with st.spinner("Generating content..."): 173 | prompt_formatted = format_prompt(prompt) 174 | content = "" 175 | if prompt_formatted != "": 176 | content = genai_api.invoke_content_creation( 177 | prompt=prompt_formatted, 178 | model_id=ai_model, 179 | access_token=st.session_state["access_token"], 180 | answer_length=answer_length, 181 | temperature=temperature, 182 | ) 183 | return content 184 | 185 | 186 | def put_prompt(item) -> None: 187 | """ 188 | Runs API call to retrieve LLM answer and references 189 | """ 190 | with st.spinner("Saving prompt template..."): 191 | success = genai_api.invoke_dynamo_put( 192 | item=item, 193 | access_token=st.session_state["access_token"], 194 | ) 195 | 196 | return success 197 | 198 | 199 | ######################### 200 | # SIDEBAR 201 | ######################### 202 | 203 | # sidebar 204 | with st.sidebar: 205 | st.header("Prompt Settings") 206 | 207 | ai_model = st.selectbox( 208 | label="Language model:", 209 | options=MODELS_DISPLAYED, 210 | format_func=lambda x: "Sagemaker: " + x if not x.startswith("Bedrock:") else x, 211 | key="ai_model", 212 | help="The search app provides flexibility to choose a large language model used for AI answers.", 213 | ) 214 | 215 | answer_length = st.slider( 216 | label="Max answer length:", 217 | value=MODEL_SPECS[st.session_state["ai_model"]]["ANSWER_LENGTH_DEFAULT"], 218 | min_value=10, 219 | max_value=4000, 220 | key="answer_length", 221 | ) 222 | temperature = st.slider( 223 | label="Temperature:", 224 | value=MODEL_SPECS[st.session_state["ai_model"]]["TEMPERATURE_DEFAULT"], 225 | min_value=0.0, 226 | max_value=1.0, 227 | key="temperature", 228 | ) 229 | 230 | with st.expander("Read more"): 231 | st.markdown( 232 | """ 233 | - **Max answer length**: maximum number of characters in the generated answer. Longer answers are more descriptive, but may require more time to be generated. 234 | - **Temperature**: temperature controls model creativity. Higher values results in more creative answers, while lower values make them more deterministic.""" 235 | ) 236 | 237 | 238 | ######################### 239 | # MAIN APP PAGE 240 | ######################### 241 | 242 | # chat history 243 | # st.markdown("Selected User:") 244 | 245 | 246 | if ( 247 | "df_selected" not in st.session_state 248 | or st.session_state["df_selected"].shape[0] == 0 249 | ): 250 | st.error("Please select a user on the 'Customer List' page first") 251 | if st.button("Go to Customer List"): 252 | switch_page("Customer List") 253 | 254 | else: 255 | st.markdown( 256 | """ 257 | This is your selected user from **Customer List** page. You can find different user attributes in the table displayed below. 258 | """ 259 | ) 260 | 261 | st.data_editor( 262 | st.session_state["df_selected"], 263 | hide_index=True, 264 | disabled=st.session_state["df_selected"].columns, 265 | height=84, 266 | use_container_width=True, 267 | ) 268 | st.markdown( 269 | "Based on the selected user, here is the recommended product to be promoted. " 270 | ) 271 | 272 | with st.expander("### Product Details", expanded=False): 273 | # todo: make it compatible with other available user-data i.e. personalized with ItemId 274 | # show default product 275 | product_info = get_product_info( 276 | st.session_state.df_selected["User.UserAttributes.Product"].item() 277 | ) 278 | st.session_state["product_info"] = product_info 279 | display_product_info(product_info) 280 | 281 | prompt_col, output_col = st.columns(2, gap="small") 282 | model_output = "" 283 | 284 | with prompt_col: 285 | prompt_area = st.text_area( 286 | "Your Prompt Template", 287 | key="prompt", 288 | height=400, 289 | ) 290 | 291 | _, col1 = st.columns([3, 1], gap="small") 292 | with col1: 293 | run_button = st.button("Run Prompt") 294 | if run_button: 295 | model_output = run_genai_prompt( 296 | ai_model=ai_model, 297 | prompt=prompt_area, 298 | answer_length=answer_length, 299 | temperature=temperature, 300 | ) 301 | st.session_state["model_output"] = model_output 302 | 303 | with output_col: 304 | model_output = st.text_area( 305 | "Model Output ", model_output, key="generated_content", height=400 306 | ) 307 | _, col = st.columns([3, 2], gap="large") 308 | with col: 309 | save_button = st.button( 310 | "Save to Catalog", 311 | key="save", 312 | help="Save this prompt template to the catalog", 313 | ) 314 | if save_button: 315 | user_id = st.session_state["user_id"] 316 | 317 | session_id, timestamp = generate_session_id(user_id=user_id) 318 | session_id = st.session_state["session_id"] 319 | if st.session_state["ai_model"] != "Bedrock: LLama2": 320 | model_output = st.session_state["model_output"] 321 | else: 322 | model_output = prompt_area 323 | item = { 324 | "session_id": session_id, 325 | "user_id": user_id, 326 | "timestamp": str(timestamp), 327 | "model": ai_model, 328 | "answer_length": str(answer_length), 329 | "temperature": str(temperature), 330 | # "Language": language, #TODO - AFTER REINVENT - UNCOMMENT 331 | # "Industry": industry, 332 | # "Task": task, 333 | # "Technique": technique, 334 | "Prompt Template": prompt_area, 335 | "Prompt": format_prompt(prompt_area), 336 | "Output": model_output, 337 | } 338 | 339 | success = put_prompt(item=item) 340 | print(success) 341 | 342 | if success: 343 | st.success("Prompt saved to the catalog successfully.") 344 | else: 345 | st.error("Failed to save prompt.") 346 | 347 | st.markdown( 348 | "If you like the output, save the prompt to the catalog which is ready to be applied for more customers in further steps." 349 | ) 350 | with st.expander("Show Logs for the Bedrock invocation!"): 351 | st.markdown( 352 | f""" 353 | To see the logs for the prompt, click on the link below: 354 | [Cloudwatch Logs](https://console.aws.amazon.com/cloudwatch/home#logsV2:log-groups/log-group/$252Faws$252Flambda$252FbdrkWorkshop-bedrock-content-generation-lambda/) 355 | """ 356 | ) 357 | 358 | st.session_state["model_output"] = model_output 359 | 360 | # add a line break for spacing 361 | st.markdown("---") 362 | 363 | with st.expander( 364 | "Pick a random user to check if your prompt template generalizes well:" 365 | ): 366 | if st.button("Pick a random user"): 367 | current_user_id = st.session_state["df_selected"]["User.UserId"].item() 368 | df_tmp = st.session_state["df"] 369 | df_excl_curr_user = df_tmp[df_tmp["User.UserId"] != current_user_id] 370 | df_selection_new = df_excl_curr_user.sample(n=1) 371 | st.session_state["df_selected"] = df_selection_new 372 | st.markdown( 373 | f"Randomly selected user: **{st.session_state['df_selected']['User.UserAttributes.FirstName'].iloc[0]} {st.session_state['df_selected']['User.UserAttributes.LastName'].iloc[0]}**" 374 | ) 375 | st.write( 376 | "head back up to the prompt template to see how your prompt template performs! " 377 | ) 378 | ######################### 379 | # FOOTNOTE 380 | ######################### 381 | 382 | # footnote 383 | st.markdown("---") 384 | footer_col1, footer_col2 = st.columns(2) 385 | 386 | # log out button 387 | with footer_col1: 388 | if st.button("Sign out"): 389 | authenticate.sign_out() 390 | st.experimental_rerun() 391 | 392 | # copyright 393 | with footer_col2: 394 | st.markdown( 395 | "
© 2023 Amazon Web Services
", 396 | unsafe_allow_html=True, 397 | ) 398 | -------------------------------------------------------------------------------- /lab2/assets/streamlit/src/components/authenticate.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utilities for Cognito authentication 3 | """ 4 | import base64 5 | import json 6 | import os 7 | from datetime import datetime 8 | 9 | import boto3 10 | import jwt 11 | import streamlit as st 12 | from botocore.exceptions import ClientError, ParamValidationError 13 | 14 | # For local testing only 15 | if "AWS_ACCESS_KEY_ID" in os.environ: 16 | print("Local Environment.") 17 | client = boto3.client( 18 | "cognito-idp", 19 | aws_access_key_id=os.environ.get("AWS_ACCESS_KEY_ID"), 20 | aws_secret_access_key=os.environ.get("AWS_SECRET_ACCESS_KEY"), 21 | aws_session_token=os.environ.get("AWS_SESSION_TOKEN"), 22 | region_name=os.environ.get("REGION"), 23 | ) 24 | else: 25 | client = boto3.client("cognito-idp") 26 | 27 | 28 | CLIENT_ID = os.environ.get("CLIENT_ID") 29 | 30 | 31 | def initialise_st_state_vars() -> None: 32 | """ 33 | Initialise Streamlit state variables 34 | """ 35 | st.session_state.setdefault("auth_code", "") 36 | st.session_state.setdefault("authenticated", "") 37 | st.session_state.setdefault("user_cognito_groups", "") 38 | st.session_state.setdefault("access_token", "") 39 | st.session_state.setdefault("refresh_token", "") 40 | st.session_state.setdefault("challenge", "") 41 | st.session_state.setdefault("mfa_setup_link", "") 42 | st.session_state.setdefault("username", "") 43 | st.session_state.setdefault("user_email", "") 44 | 45 | 46 | def verify_access_token(token: str): 47 | """ 48 | Verify if token duration has expired 49 | 50 | Parameters 51 | ---------- 52 | token : str 53 | jwt token to verify 54 | 55 | Returns 56 | ------- 57 | _type_ 58 | _description_ 59 | """ 60 | 61 | decoded_data = jwt.decode( 62 | token, algorithms=["RS256"], options={"verify_signature": False} 63 | ) 64 | 65 | expires = decoded_data["exp"] 66 | 67 | now = datetime.now().timestamp() 68 | 69 | return (expires - now) > 0 70 | 71 | 72 | def update_access_token() -> None: 73 | """ 74 | Get new access token using the refresh token 75 | """ 76 | try: 77 | response = client.initiate_auth( 78 | AuthFlow="REFRESH_TOKEN_AUTH", 79 | AuthParameters={"REFRESH_TOKEN": st.session_state["refresh_token"]}, 80 | ClientId=CLIENT_ID, 81 | ) 82 | 83 | except ClientError: 84 | st.session_state["authenticated"] = False 85 | st.session_state["access_token"] = "" 86 | st.session_state["user_cognito_groups"] = [] 87 | st.session_state["refresh_token"] = "" 88 | else: 89 | access_token = response["AuthenticationResult"]["AccessToken"] 90 | id_token = response["AuthenticationResult"]["IdToken"] 91 | user_attributes_dict = get_user_attributes(id_token) 92 | st.session_state["access_token"] = access_token 93 | st.session_state["authenticated"] = True 94 | st.session_state["user_cognito_groups"] = None 95 | if "user_cognito_groups" in user_attributes_dict: 96 | st.session_state["user_cognito_groups"] = user_attributes_dict[ 97 | "user_cognito_groups" 98 | ] 99 | st.session_state["user_id"] = "" 100 | if "username" in user_attributes_dict: 101 | st.session_state["user_id"] = user_attributes_dict["username"] 102 | if "user_email" in user_attributes_dict: 103 | st.session_state["user_email"] = user_attributes_dict["email"] 104 | 105 | 106 | def pad_base64(data: str) -> str: 107 | """ 108 | Decode access token to JWT to get user's cognito groups 109 | Ref - https://gist.github.com/GuillaumeDerval/b300af6d4f906f38a051351afab3b95c 110 | 111 | Parameters 112 | ---------- 113 | data : str 114 | base64 token string 115 | 116 | Returns 117 | ------- 118 | str 119 | padded token string 120 | """ 121 | 122 | missing_padding = len(data) % 4 123 | if missing_padding != 0: 124 | data += "=" * (4 - missing_padding) 125 | return data 126 | 127 | 128 | def get_user_attributes(id_token: str) -> dict: 129 | """ 130 | Decode id token to get user cognito groups. 131 | 132 | Parameters 133 | ---------- 134 | id_token : str 135 | ID token of a successfully authenticated user 136 | 137 | Returns 138 | ------- 139 | dict 140 | Dictionary with two keys (username, and list of all the cognito groups the user belongs to) 141 | """ 142 | 143 | user_attrib_dict = {} 144 | 145 | if id_token != "": 146 | _, payload, _ = id_token.split(".") 147 | printable_payload = base64.urlsafe_b64decode(pad_base64(payload)) 148 | payload_dict = dict(json.loads(printable_payload)) 149 | print(payload_dict) 150 | if "cognito:groups" in payload_dict: 151 | user_cognito_groups = list(payload_dict["cognito:groups"]) 152 | user_attrib_dict["user_cognito_groups"] = user_cognito_groups 153 | if "cognito:username" in payload_dict: 154 | username = payload_dict["cognito:username"] 155 | user_attrib_dict["username"] = username 156 | print(username) 157 | print("LOGGED IN SUCESSFULLY") 158 | if "email" in payload_dict: 159 | user_email = payload_dict["email"] 160 | user_attrib_dict["user_email"] = user_email 161 | return user_attrib_dict 162 | 163 | 164 | def set_st_state_vars() -> None: 165 | """ 166 | Sets the streamlit state variables after user authentication. 167 | """ 168 | 169 | initialise_st_state_vars() 170 | 171 | if "access_token" in st.session_state and st.session_state["access_token"] != "": 172 | # If there is an access token, check if still valid 173 | is_valid = verify_access_token(st.session_state["access_token"]) 174 | 175 | # If token not valid anymore create a new one with refresh token 176 | if not is_valid: 177 | update_access_token() 178 | 179 | 180 | def login_successful(response: dict) -> None: 181 | """ 182 | Update streamlit state variables on successful login 183 | 184 | Parameters 185 | ---------- 186 | response : dict 187 | boto3 response of the successful login API call 188 | """ 189 | 190 | access_token = response["AuthenticationResult"]["AccessToken"] 191 | id_token = response["AuthenticationResult"]["IdToken"] 192 | refresh_token = response["AuthenticationResult"]["RefreshToken"] 193 | 194 | user_attributes_dict = get_user_attributes(id_token) 195 | 196 | if access_token != "": 197 | st.session_state["access_token"] = access_token 198 | st.session_state["authenticated"] = True 199 | st.session_state["user_cognito_groups"] = None 200 | if "user_cognito_groups" in user_attributes_dict: 201 | st.session_state["user_cognito_groups"] = user_attributes_dict[ 202 | "user_cognito_groups" 203 | ] 204 | st.session_state["user_id"] = "" 205 | if "username" in user_attributes_dict: 206 | st.session_state["user_id"] = user_attributes_dict["username"] 207 | st.session_state["refresh_token"] = refresh_token 208 | if "user_email" in user_attributes_dict: 209 | st.session_state["user_email"] = user_attributes_dict["user_email"] 210 | 211 | 212 | def associate_software_token(user, session): 213 | """ 214 | Associate new MFA token to user during MFA setup 215 | 216 | Parameters 217 | ---------- 218 | user : _type_ 219 | the user from MFA_SETUP challenge 220 | session : _type_ 221 | valid user session 222 | 223 | Returns 224 | ------- 225 | _type_ 226 | New valid user session 227 | """ 228 | 229 | response = client.associate_software_token(Session=session) 230 | 231 | secret_code = response["SecretCode"] 232 | st.session_state["mfa_setup_link"] = f"otpauth://totp/{user}?secret={secret_code}" 233 | 234 | return response["Session"] 235 | 236 | 237 | def create_user(username: str, pwd: str, email: str) -> None: 238 | """ 239 | Create a new user account 240 | 241 | Parameters 242 | ---------- 243 | username : str 244 | user provided username 245 | pwd : str 246 | user provided password 247 | email : str 248 | user provided email 249 | """ 250 | try: 251 | response = client.sign_up( 252 | ClientId=CLIENT_ID, 253 | Username=username, 254 | Password=pwd, 255 | UserAttributes=[ 256 | {"Name": "email", "Value": email}, 257 | {"Name": "preferred_username", "Value": username}, 258 | ], 259 | ) 260 | 261 | st.session_state["account_created"] = True 262 | return True, response 263 | except ClientError as e: 264 | st.session_state["account_created"] = False 265 | return False, e 266 | 267 | 268 | def confirm_sign_up(username: str, code: str) -> None: 269 | """ 270 | Confirm user account creation 271 | 272 | Parameters 273 | ---------- 274 | username : str 275 | user provided username 276 | code : str 277 | user provided confirmation code 278 | """ 279 | 280 | try: 281 | response = client.confirm_sign_up( 282 | ClientId=CLIENT_ID, 283 | Username=username, 284 | ConfirmationCode=code, 285 | ) 286 | 287 | st.session_state["account_confirmed"] = True 288 | return True, response 289 | except ClientError as e: 290 | st.session_state["account_confirmed"] = False 291 | return False, e 292 | 293 | 294 | def sign_in(username: str, pwd: str) -> None: 295 | """ 296 | User sign in with user name and password, will store following challenge parameters in state 297 | 298 | Parameters 299 | ---------- 300 | username : str 301 | user provided username 302 | pwd : str 303 | user provided password 304 | """ 305 | 306 | try: 307 | response = client.initiate_auth( 308 | AuthFlow="USER_PASSWORD_AUTH", 309 | AuthParameters={"USERNAME": username, "PASSWORD": pwd}, 310 | ClientId=CLIENT_ID, 311 | ) 312 | 313 | except ClientError: 314 | st.session_state["authenticated"] = False 315 | 316 | else: 317 | if "ChallengeName" in response: 318 | st.session_state["challenge"] = response["ChallengeName"] 319 | 320 | if "USER_ID_FOR_SRP" in response["ChallengeParameters"]: 321 | st.session_state["challenge_user"] = response["ChallengeParameters"][ 322 | "USER_ID_FOR_SRP" 323 | ] 324 | 325 | if response["ChallengeName"] == "MFA_SETUP": 326 | session = associate_software_token( 327 | st.session_state["challenge_user"], response["Session"] 328 | ) 329 | st.session_state["session"] = session 330 | else: 331 | st.session_state["session"] = response["Session"] 332 | 333 | else: 334 | login_successful(response) 335 | 336 | 337 | def verify_token(token: str): 338 | """ 339 | Verify MFA token to complete MFA setup 340 | 341 | Parameters 342 | ---------- 343 | token : str 344 | token from user MFA app 345 | 346 | Returns 347 | ------- 348 | _type_ 349 | success : bool 350 | True if succeeded, False otherwise 351 | message : str 352 | Error message 353 | """ 354 | 355 | success = False 356 | message = "" 357 | try: 358 | response = client.verify_software_token( 359 | Session=st.session_state["session"], 360 | UserCode=token, 361 | ) 362 | 363 | except ClientError as e: 364 | if e.response["Error"]["Code"] == "InvalidParameterException": 365 | message = "Please enter 6 or more digit numbers." 366 | else: 367 | message = ( 368 | "Session expired, please reload the page and scan the QR code again." 369 | ) 370 | except ParamValidationError: 371 | message = "Please enter 6 or more digit numbers." 372 | else: 373 | if response["Status"] == "SUCCESS": 374 | st.session_state["session"] = response["Session"] 375 | success = True 376 | 377 | return success, message 378 | 379 | 380 | def setup_mfa(): 381 | """ 382 | Reply to MFA setup challenge 383 | The current session has to be updated by verify token function 384 | 385 | Returns 386 | ------- 387 | _type_ 388 | success : bool 389 | True if succeeded, False otherwise 390 | message : str 391 | Error message 392 | """ 393 | 394 | message = "" 395 | success = False 396 | 397 | try: 398 | response = client.respond_to_auth_challenge( 399 | ClientId=CLIENT_ID, 400 | ChallengeName="MFA_SETUP", 401 | Session=st.session_state["session"], 402 | ChallengeResponses={ 403 | "USERNAME": st.session_state["challenge_user"], 404 | }, 405 | ) 406 | 407 | except ClientError: 408 | message = "Session expired, please sign out and in again." 409 | else: 410 | success = True 411 | st.session_state["challenge"] = "" 412 | st.session_state["session"] = "" 413 | login_successful(response) 414 | 415 | return success, message 416 | 417 | 418 | def sign_in_with_token(token: str): 419 | """ 420 | Verify MFA token and complete login process 421 | 422 | Parameters 423 | ---------- 424 | token : str 425 | token from user MFA app 426 | 427 | Returns 428 | ------- 429 | _type_ 430 | success : bool 431 | True if succeeded, False otherwise 432 | message : str 433 | Error message 434 | """ 435 | 436 | message = "" 437 | success = False 438 | try: 439 | response = client.respond_to_auth_challenge( 440 | ClientId=CLIENT_ID, 441 | ChallengeName="SOFTWARE_TOKEN_MFA", 442 | Session=st.session_state["session"], 443 | ChallengeResponses={ 444 | "USERNAME": st.session_state["challenge_user"], 445 | "SOFTWARE_TOKEN_MFA_CODE": token, 446 | }, 447 | ) 448 | 449 | except ClientError: 450 | message = "Session expired, please sign out and in again." 451 | else: 452 | success = True 453 | st.session_state["challenge"] = "" 454 | st.session_state["session"] = "" 455 | login_successful(response) 456 | 457 | return success, message 458 | 459 | 460 | def reset_password(password: str): 461 | """ 462 | Reset password on first connection, will store parameters of following challenge 463 | 464 | Parameters 465 | ---------- 466 | password : str 467 | new password to set 468 | 469 | Returns 470 | ------- 471 | _type_ 472 | success : bool 473 | True if succeeded, False otherwise 474 | message : str 475 | Error message 476 | """ 477 | 478 | message = "" 479 | success = False 480 | 481 | try: 482 | response = client.respond_to_auth_challenge( 483 | ClientId=CLIENT_ID, 484 | ChallengeName="NEW_PASSWORD_REQUIRED", 485 | Session=st.session_state["session"], 486 | ChallengeResponses={ 487 | "NEW_PASSWORD": password, 488 | "USERNAME": st.session_state["challenge_user"], 489 | }, 490 | ) 491 | 492 | except ClientError as e: 493 | if e.response["Error"]["Code"] == "InvalidPasswordException": 494 | message = e.response["Error"]["Message"] 495 | else: 496 | message = "Session expired, please sign out and in again." 497 | else: 498 | success = True 499 | 500 | if "ChallengeName" in response: 501 | st.session_state["challenge"] = response["ChallengeName"] 502 | 503 | if response["ChallengeName"] == "MFA_SETUP": 504 | session = associate_software_token( 505 | st.session_state["challenge_user"], response["Session"] 506 | ) 507 | st.session_state["session"] = session 508 | else: 509 | st.session_state["session"] = response["Session"] 510 | 511 | else: 512 | st.session_state["challenge"] = "" 513 | st.session_state["session"] = "" 514 | 515 | return success, message 516 | 517 | 518 | def sign_out() -> None: 519 | """ 520 | Sign out user by updating all relevant state parameters 521 | """ 522 | if st.session_state["refresh_token"] != "": 523 | client.revoke_token( 524 | Token=st.session_state["refresh_token"], 525 | ClientId=CLIENT_ID, 526 | ) 527 | 528 | st.session_state["authenticated"] = False 529 | st.session_state["user_cognito_groups"] = [] 530 | st.session_state["access_token"] = "" 531 | st.session_state["refresh_token"] = "" 532 | st.session_state["challenge"] = "" 533 | st.session_state["session"] = "" 534 | st.session_state["user_email"] = "" 535 | -------------------------------------------------------------------------------- /lab2/infra/constructs/bdrk_reinvent_api.py: -------------------------------------------------------------------------------- 1 | """ 2 | bdrk_reinvent API constructs 3 | """ 4 | 5 | import aws_cdk.aws_apigatewayv2_alpha as _apigw 6 | import aws_cdk.aws_apigatewayv2_integrations_alpha as _integrations 7 | from aws_cdk import aws_apigateway 8 | from aws_cdk import Aws, CfnOutput, Duration, RemovalPolicy 9 | from aws_cdk import aws_cognito as cognito 10 | from aws_cdk import aws_dynamodb as ddb 11 | from aws_cdk import aws_sns as sns 12 | from aws_cdk import aws_iam as iam 13 | from aws_cdk import aws_lambda as _lambda 14 | from aws_cdk import aws_logs as logs 15 | from aws_cdk import custom_resources as cr 16 | from aws_cdk.aws_apigatewayv2_authorizers_alpha import HttpUserPoolAuthorizer 17 | from aws_cdk import aws_kms as kms 18 | from constructs import Construct 19 | 20 | QUERY_BEDROCK_TIMEOUT = 900 21 | 22 | 23 | class bdrk_reinventAPIConstructs(Construct): 24 | def __init__( 25 | self, 26 | scope: Construct, 27 | construct_id: str, 28 | stack_name, 29 | # s3_data_bucket: _s3.Bucket, 30 | layers: Construct, 31 | bedrock_region: str, 32 | bedrock_role_arn: str = None, 33 | mfa_enabled: bool = True, 34 | **kwargs, 35 | ) -> None: 36 | super().__init__(scope, construct_id, **kwargs) 37 | 38 | # self.s3_data_bucket = s3_data_bucket 39 | 40 | self.bedrock_region = bedrock_region 41 | self.bedrock_role_arn = bedrock_role_arn 42 | 43 | self.stack_name = stack_name 44 | self.layers = layers 45 | 46 | self.prefix = stack_name[:16] 47 | self.create_cognito_user_pool(mfa_enabled) 48 | 49 | ## **************** Create resources **************** 50 | self.create_dynamodb() 51 | self.create_sns_topic() 52 | self.create_roles() 53 | self.create_lambda_functions() 54 | 55 | self.authorizer = HttpUserPoolAuthorizer( 56 | "BooksAuthorizer", self.user_pool, user_pool_clients=[self.user_pool_client] 57 | ) 58 | 59 | self.create_http_api() 60 | 61 | # Outputs 62 | CfnOutput( 63 | self, 64 | "API Endpoint", 65 | description="API Endpoint", 66 | value=self.api_uri, 67 | ) 68 | CfnOutput(self, "Cognito Client ID", description="Cognito Client ID", value=self.client_id) 69 | 70 | def create_http_api(self): 71 | log_group = logs.LogGroup( 72 | self, 73 | "ApiAccessLogs", 74 | log_group_name=f"{self.stack_name}-api-access-logs", 75 | retention=logs.RetentionDays.ONE_MONTH, 76 | ) 77 | 78 | # Create the HTTP API with CORS 79 | http_api = _apigw.HttpApi( 80 | self, 81 | f"{self.stack_name}-http-api", 82 | default_authorizer=self.authorizer, 83 | cors_preflight=_apigw.CorsPreflightOptions( 84 | allow_methods=[_apigw.CorsHttpMethod.POST], 85 | allow_origins=["*"], 86 | max_age=Duration.days(10), 87 | ), 88 | ) 89 | 90 | # add content/bedrock to POST / 91 | http_api.add_routes( 92 | path="/content/bedrock", 93 | methods=[_apigw.HttpMethod.POST], 94 | integration=_integrations.HttpLambdaIntegration( 95 | "LambdaProxyIntegration", handler=self.bedrock_content_generation_lambda 96 | ), 97 | ) 98 | 99 | # add dynamo/put to POST / 100 | http_api.add_routes( 101 | path="/dynamo/put", 102 | methods=[_apigw.HttpMethod.POST], 103 | integration=_integrations.HttpLambdaIntegration("LambdaProxyIntegration", handler=self.prompt_ddb_lambda), 104 | ) 105 | 106 | # add dynamo/put to POST / 107 | http_api.add_routes( 108 | path="/dynamo/get", 109 | methods=[_apigw.HttpMethod.GET], 110 | integration=_integrations.HttpLambdaIntegration("LambdaProxyIntegration", handler=self.prompt_ddb_lambda), 111 | ) 112 | 113 | # add dynamo/put to POST / 114 | http_api.add_routes( 115 | path="/dynamo/delete", 116 | methods=[_apigw.HttpMethod.DELETE], 117 | integration=_integrations.HttpLambdaIntegration("LambdaProxyIntegration", handler=self.prompt_ddb_lambda), 118 | ) 119 | 120 | # add sns/put to POST / 121 | http_api.add_routes( 122 | path="/sns/put", 123 | methods=[_apigw.HttpMethod.POST], 124 | integration=_integrations.HttpLambdaIntegration("LambdaProxyIntegration", handler=self.sns_topic_lambda), 125 | ) 126 | 127 | self.api_uri = http_api.api_endpoint 128 | 129 | def create_cognito_user_pool(self, mfa_enabled): 130 | # Cognito User Pool 131 | 132 | # Define the password policy 133 | password_policy = cognito.PasswordPolicy( 134 | min_length=8, # Minimum length of 8 characters 135 | require_lowercase=True, # Require lowercase characters 136 | require_uppercase=True, # Require uppercase characters 137 | require_digits=True, # Require numeric characters 138 | require_symbols=True, # Require special characters 139 | ) 140 | 141 | if mfa_enabled: 142 | self.user_pool = cognito.UserPool( 143 | self, 144 | f"{self.prefix}-user-pool", 145 | user_pool_name=f"{self.prefix}-user-pool", 146 | auto_verify=cognito.AutoVerifiedAttrs(email=True), 147 | mfa=cognito.Mfa.REQUIRED, 148 | mfa_second_factor=cognito.MfaSecondFactor(sms=False, otp=True), 149 | self_sign_up_enabled=True, 150 | password_policy=password_policy, 151 | advanced_security_mode=cognito.AdvancedSecurityMode.ENFORCED, 152 | ) 153 | else: 154 | self.user_pool = cognito.UserPool( 155 | self, 156 | f"{self.prefix}-user-pool", 157 | user_pool_name=f"{self.prefix}-user-pool", 158 | auto_verify=cognito.AutoVerifiedAttrs(email=True), 159 | self_sign_up_enabled=True, 160 | password_policy=password_policy, 161 | advanced_security_mode=cognito.AdvancedSecurityMode.ENFORCED, 162 | ) 163 | self.user_pool_client = self.user_pool.add_client( 164 | "customer-app-client", 165 | user_pool_client_name=f"{self.prefix}-client", 166 | generate_secret=False, 167 | auth_flows=cognito.AuthFlow(user_password=True), 168 | ) 169 | 170 | self.client_id = self.user_pool_client.user_pool_client_id 171 | 172 | ## **************** DynamoDB **************** 173 | def create_dynamodb(self): 174 | self.chat_history_table = ddb.Table( 175 | self, 176 | f"{self.stack_name}-chat-history", 177 | table_name=f"{self.stack_name}-chat-history", 178 | partition_key=ddb.Attribute(name="SessionId", type=ddb.AttributeType.STRING), 179 | table_class=ddb.TableClass.STANDARD, 180 | billing_mode=ddb.BillingMode("PAY_PER_REQUEST"), 181 | removal_policy=RemovalPolicy.DESTROY, 182 | point_in_time_recovery=True, 183 | ) 184 | 185 | self.prompts_table = ddb.Table( 186 | self, 187 | f"{self.stack_name}-prompt-db", 188 | # table_name=f"{self.stack_name}-prompts-db", 189 | partition_key=ddb.Attribute(name="session_id", type=ddb.AttributeType.STRING), 190 | # sort_key=ddb.Attribute(name="Industry", type=ddb.AttributeType.STRING), 191 | table_class=ddb.TableClass.STANDARD, 192 | billing_mode=ddb.BillingMode("PAY_PER_REQUEST"), 193 | removal_policy=RemovalPolicy.DESTROY, 194 | point_in_time_recovery=True, 195 | ) 196 | 197 | ## **************** Create SNS Topic **************** 198 | def create_sns_topic(self): 199 | # Create a new KMS Key for encryption 200 | self.kms_key = kms.Key( 201 | self, 202 | "EmailTopicEncryptionKey", 203 | description="KMS key for SNS topic encryption", 204 | enable_key_rotation=True, # Optional: Automatically rotates the key every year 205 | ) 206 | 207 | # Create SNS Topic with server-side encryption enabled 208 | self.sns_topic = sns.Topic( 209 | self, 210 | "EmailTopic", 211 | topic_name=f"{self.stack_name}-email-topic", 212 | master_key=self.kms_key, # Use the created KMS key for encryption 213 | ) 214 | 215 | ## **************** Lambda Functions **************** 216 | def create_lambda_functions(self): 217 | ## ********* Bedrock ********* 218 | self.bedrock_content_generation_lambda = _lambda.Function( 219 | self, 220 | f"{self.stack_name}-bedrock-content-generation-lambda", 221 | runtime=_lambda.Runtime.PYTHON_3_9, 222 | code=_lambda.Code.from_asset("./assets/lambda/genai/bedrock_content_generation_lambda"), 223 | handler="bedrock_content_generation_lambda.lambda_handler", 224 | function_name=f"{self.stack_name}-bedrock-content-generation-lambda", 225 | memory_size=3008, 226 | timeout=Duration.seconds(QUERY_BEDROCK_TIMEOUT), 227 | environment={ 228 | "BEDROCK_REGION": self.bedrock_region, 229 | "BEDROCK_ROLE_ARN": str(self.bedrock_role_arn), 230 | }, 231 | role=self.bedrock_content_generation_role, 232 | layers=[ 233 | self.layers.bedrock_compatible_sdk, 234 | ], 235 | ) 236 | self.bedrock_content_generation_lambda.add_alias( 237 | "Warm", 238 | provisioned_concurrent_executions=0, 239 | description="Alias used for Lambda provisioned concurrency", 240 | ) 241 | 242 | ## ********* Dynamo connection DDB ********* 243 | self.prompt_ddb_lambda = _lambda.Function( 244 | self, 245 | f"{self.stack_name}-prompt-ddb-lambda", 246 | runtime=_lambda.Runtime.PYTHON_3_11, 247 | code=_lambda.Code.from_asset("./assets/lambda/db_connections/prompt_lambda"), 248 | handler="prompt_lambda.lambda_handler", 249 | function_name=f"{self.stack_name}-prompt-lambda", 250 | memory_size=3008, 251 | timeout=Duration.seconds(20), 252 | environment={ 253 | "TABLE_NAME": self.prompts_table.table_name, 254 | }, 255 | role=self.lambda_DDB_role, 256 | ) 257 | self.prompt_ddb_lambda.add_alias( 258 | "Warm", 259 | provisioned_concurrent_executions=0, 260 | description="Alias used for Lambda provisioned concurrency", 261 | ) 262 | 263 | ## ********* SNS Topic Lambda ********* 264 | self.sns_topic_lambda = _lambda.Function( 265 | self, 266 | f"{self.stack_name}-sns-topic-lambda", 267 | runtime=_lambda.Runtime.PYTHON_3_11, 268 | code=_lambda.Code.from_asset("./assets/lambda/sns_topic_lambda"), 269 | handler="sns_topic_lambda.lambda_handler", 270 | function_name=f"{self.stack_name}-sns-topic-lambda", 271 | memory_size=128, 272 | timeout=Duration.seconds(20), 273 | environment={ 274 | "SNS_TOPIC_ARN": self.sns_topic.topic_arn, 275 | }, 276 | role=self.sns_topic_role, 277 | ) 278 | 279 | ## **************** IAM Permissions **************** 280 | def create_roles(self: str): 281 | ## ********* IAM Roles ********* 282 | self.bedrock_content_generation_role = iam.Role( 283 | self, 284 | f"{self.stack_name}-bedrock-content-generation-role", 285 | role_name=f"{self.stack_name}-bedrock-content-generation-role", 286 | assumed_by=iam.CompositePrincipal( 287 | iam.ServicePrincipal("lambda.amazonaws.com"), 288 | ), 289 | ) 290 | self.lambda_DDB_role = iam.Role( 291 | self, 292 | f"{self.stack_name}-DDB-role", 293 | role_name=f"{self.stack_name}-DDB-role", 294 | assumed_by=iam.CompositePrincipal( 295 | iam.ServicePrincipal("lambda.amazonaws.com"), 296 | ), 297 | ) 298 | 299 | self.sns_topic_role = iam.Role( 300 | self, 301 | f"{self.stack_name}-sns-topic-role", 302 | role_name=f"{self.stack_name}-sns-topic-role", 303 | assumed_by=iam.CompositePrincipal( 304 | iam.ServicePrincipal("lambda.amazonaws.com"), 305 | ), 306 | ) 307 | 308 | ## ********* Cloudwatch ********* 309 | # Content Gen 310 | cloudwatch_access_docpolicy_content_gen = iam.PolicyDocument( 311 | statements=[ 312 | # Allow creating log groups at the account and region level 313 | iam.PolicyStatement( 314 | effect=iam.Effect.ALLOW, 315 | actions=["logs:CreateLogGroup"], 316 | resources=[f"arn:aws:logs:{Aws.REGION}:{Aws.ACCOUNT_ID}:*"], 317 | ), 318 | # Allow creating log streams and putting log events for specific Lambda functions 319 | iam.PolicyStatement( 320 | effect=iam.Effect.ALLOW, 321 | actions=["logs:CreateLogStream", "logs:PutLogEvents"], 322 | resources=[ 323 | f"arn:aws:logs:{Aws.REGION}:{Aws.ACCOUNT_ID}:log-group:/aws/lambda/{self.stack_name}-bedrock-content-generation-lambda:*" 324 | ], 325 | ), 326 | ] 327 | ) 328 | 329 | cloudwatch_access_policy_content = iam.Policy( 330 | self, 331 | f"{self.stack_name}-cloudwatch-access-policy-content-gen", 332 | policy_name=f"{self.stack_name}-cloudwatch-access-policy-content-gen", 333 | document=cloudwatch_access_docpolicy_content_gen, 334 | ) 335 | 336 | # Prompt DDB 337 | cloudwatch_access_doc_policy_prompt_ddb = iam.PolicyDocument( 338 | statements=[ 339 | iam.PolicyStatement( 340 | effect=iam.Effect.ALLOW, 341 | actions=["logs:CreateLogStream", "logs:PutLogEvents", "logs:CreateLogGroup"], 342 | resources=[ 343 | f"arn:aws:logs:{Aws.REGION}:{Aws.ACCOUNT_ID}:log-group:/aws/lambda/{self.stack_name}-prompt-lambda" 344 | ], 345 | ), 346 | ] 347 | ) 348 | 349 | cloudwatch_access_policy_prompt_ddb = iam.Policy( 350 | self, 351 | f"{self.stack_name}-cloudwatch-access-policy-prompt-ddb", 352 | policy_name=f"{self.stack_name}-cloudwatch-access-policy-prompt-ddb", 353 | document=cloudwatch_access_doc_policy_prompt_ddb, 354 | ) 355 | 356 | # SNS Topic 357 | cloudwatch_access_doc_sns_topic = iam.PolicyDocument( 358 | statements=[ 359 | # Allow creating log groups at the account and region level 360 | iam.PolicyStatement( 361 | effect=iam.Effect.ALLOW, 362 | actions=["logs:CreateLogGroup"], 363 | resources=[f"arn:aws:logs:{Aws.REGION}:{Aws.ACCOUNT_ID}:*"], 364 | ), 365 | # Allow creating log streams and putting log events for specific Lambda functions 366 | iam.PolicyStatement( 367 | effect=iam.Effect.ALLOW, 368 | actions=["logs:CreateLogStream", "logs:PutLogEvents"], 369 | resources=[ 370 | f"arn:aws:logs:{Aws.REGION}:{Aws.ACCOUNT_ID}:log-group:/aws/lambda/{self.stack_name}-sns-topic-lambda:*" 371 | ], 372 | ), 373 | ] 374 | ) 375 | 376 | cloudwatch_access_policy_sns_topic = iam.Policy( 377 | self, 378 | f"{self.stack_name}-cloudwatch-access-policy-sns-topic", 379 | policy_name=f"{self.stack_name}-cloudwatch-access-policy-sns-topic", 380 | document=cloudwatch_access_doc_sns_topic, 381 | ) 382 | self.bedrock_content_generation_role.attach_inline_policy(cloudwatch_access_policy_content) 383 | self.lambda_DDB_role.attach_inline_policy(cloudwatch_access_policy_prompt_ddb) 384 | self.sns_topic_role.attach_inline_policy(cloudwatch_access_policy_sns_topic) 385 | 386 | ## ********* DynamoDB ********* 387 | prompt_db_docpolicy = iam.PolicyDocument( 388 | statements=[ 389 | iam.PolicyStatement( 390 | actions=[ 391 | "dynamodb:PutItem", 392 | "dynamodb:GetItem", 393 | "dynamodb:DeleteItem", 394 | "dynamodb:UpdateItem", 395 | "dynamodb:Scan", 396 | ], 397 | resources=[ 398 | self.prompts_table.table_arn, 399 | ], 400 | ) 401 | ] 402 | ) 403 | prompt_db_policy = iam.Policy( 404 | self, 405 | f"{self.stack_name}-prompt_db_policy", 406 | policy_name=f"{self.stack_name}-prompt_db_policy", 407 | document=prompt_db_docpolicy, 408 | ) 409 | self.lambda_DDB_role.attach_inline_policy(prompt_db_policy) 410 | ## ********* SNS Topic ********* 411 | sns_topic_docpolicy = iam.PolicyDocument( 412 | statements=[ 413 | iam.PolicyStatement( 414 | actions=["sns:Publish", "sns:Subscribe", "sns:ListSubscriptionsByTopic"], 415 | resources=[ 416 | self.sns_topic.topic_arn, 417 | ], 418 | ), 419 | iam.PolicyStatement( 420 | actions=["kms:GenerateDataKey", "kms:Decrypt"], 421 | resources=[ 422 | self.kms_key.key_arn, 423 | ], 424 | ), 425 | ] 426 | ) 427 | 428 | sns_topic_policy = iam.Policy( 429 | self, 430 | f"{self.stack_name}-sns-topic-policy", 431 | policy_name=f"{self.stack_name}-sns-topic-policy", 432 | document=sns_topic_docpolicy, 433 | ) 434 | self.sns_topic_role.attach_inline_policy(sns_topic_policy) 435 | 436 | ## ********* Bedrock ********* 437 | bedrock_access_docpolicy = iam.PolicyDocument( 438 | statements=[ 439 | iam.PolicyStatement( 440 | actions=[ 441 | "bedrock:ListFoundationModels", 442 | ], 443 | resources=["*"], 444 | ), 445 | iam.PolicyStatement( 446 | actions=[ 447 | "bedrock:InvokeModel", 448 | ], 449 | resources=[f"arn:aws:bedrock:{Aws.REGION}::foundation-model/*"], 450 | ), 451 | ] 452 | ) 453 | 454 | bedrock_access_policy = iam.Policy( 455 | self, 456 | f"{self.stack_name}-bedrock-access-policy", 457 | policy_name=f"{self.stack_name}-bedrock-access-policy", 458 | document=bedrock_access_docpolicy, 459 | ) 460 | self.bedrock_content_generation_role.attach_inline_policy(bedrock_access_policy) 461 | -------------------------------------------------------------------------------- /lab1/ML-blog.txt: -------------------------------------------------------------------------------- 1 | Announcing New Tools to Help Every Business Embrace Generative AI From startups to enterprises, organizations of all sizes are getting started with generative AI. They want to capitalize on generative AI and translate the momentum from betas, prototypes, and demos into real-world productivity gains and innovations. But what do organizations need to bring generative AI into the enterprise and make it real? When we talk to customers, they tell us they need security and privacy, scale and price-performance, and most importantly tech that is relevant to their business. We are excited to announce new capabilities and services today to allow organizations big and small to use generative AI in creative ways, building new applications and improving how they work. At AWS, we are hyper-focused on helping our customers in a few ways: Making it easy to build generative AI applications with security and privacy built in Focusing on the most performant, low cost infrastructure for generative AI so you can train your own models and run inference at scale Providing generative AI-powered applications for the enterprise to transform how work gets done Enabling data as your differentiator to customize foundation models (FMs) and make them an expert on your business, your data, and your company To help a broad range of organizations build differentiated generative AI experiences, AWS has been working hand-in-hand with our customers, including BBVA, Thomson Reuters, United Airlines, Philips, and LexisNexis Legal & Professional. And with the new capabilities launched today, we look forward to enhanced productivity, improved customer engagement, and more personalized experiences that will transform how companies get work done. Announcing the general availability of Amazon Bedrock, the easiest way to build generative AI applications with security and privacy built in Customers are excited and optimistic about the value that generative AI can bring to the enterprise. They are diving deep into the technology to learn the steps they need to take to build a generative AI system in production. While recent advancements in generative AI have captured widespread attention, many businesses have not been able to take part in this transformation. Customers tell us they need a choice of models, security and privacy assurances, a data-first approach, cost-effective ways to run models, and capabilities like prompt engineering, retrieval augmented generation (RAG), agents, and more to create customized applications. That is why on April 13, 2023, we announced Amazon Bedrock, the easiest way to build and scale generative AI applications with foundation models. Amazon Bedrock is a fully managed service that offers a choice of high-performing foundation models from leading providers like AI21 Labs, Anthropic, Cohere, Meta, Stability AI, and Amazon, along with a broad set of capabilities that customers need to build generative AI applications, simplifying development while maintaining privacy and security. Additionally, as part of a recently announced strategic collaboration, all future FMs from Anthropic will be available within Amazon Bedrock with early access to unique features for model customization and fine-tuning capabilities. Since April, we have seen firsthand how startups like Coda, Hurone AI, and Nexxiot; large enterprises like adidas, GoDaddy, Clariant, and Broadridge; and partners like Accenture, BCG, Leidos, and Mission Cloud are already using Amazon Bedrock to securely build generative AI applications across industries. Independent software vendors (ISVs) like Salesforce are now securely integrating with Amazon Bedrock to enable their customers to power generative AI applications. Customers are applying generative AI to new use cases; for example, Lonely Planet, a premier travel media company, worked with our Generative AI Innovation Center to introduce a scalable AI platform that organizes book content in minutes to deliver cohesive, highly accurate travel recommendations, reducing itinerary generation costs by nearly 80%. And since then, we have continued to add new capabilities, like agents for Amazon Bedrock, as well as support for new models, like Cohere and the latest models from Anthropic, to offer our customers more choice and make it easier to create generative AI-based applications. Agents for Bedrock are a game changer, allowing LLMs to complete complex tasks based on your own data and APIs, privately, securely, with setup in minutes (no training or fine tuning required). Today, we are excited to share new announcements that make it easier to bring generative AI to your organization: General availability of Amazon Bedrock to help even more customers build and scale generative AI applications Expanded model choice with Llama 2 (coming in the next few weeks) and Amazon Titan Embeddings gives customers greater choice and flexibility to find the right model for each use case and power RAG for better results Amazon Bedrock is a HIPAA eligible service and can be used in compliance with GDPR, allowing even more customers to benefit from generative AI Provisioned throughput to ensure a consistent user experience even during peak traffic times With the general availability of Amazon Bedrock, more customers will have access to Bedrock’s comprehensive capabilities. Customers can easily experiment with a variety of top FMs, customize them privately with their data using techniques such as fine tuning and RAG, and create managed agents that execute complex business tasks—from booking travel and processing insurance claims to creating ad campaigns and managing inventory—all without writing any code. Since Amazon Bedrock is serverless, customers don’t have to manage any infrastructure, and they can securely integrate and deploy generative AI capabilities into their applications using the AWS services they are already familiar with. Second, model choice has been a cornerstone of what makes Amazon Bedrock a unique, differentiated service for our customers. This early in the adoption of generative AI, there is no single model that unlocks all the value of generative AI, and customers need the ability to work with a range of high-performing models. We are excited to announce the general availability of Amazon Titan Embeddings and coming in the next few weeks availability of Llama 2, Meta’s next generation large language model (LLM) – joining existing model providers AI21 Labs, Anthropic, Cohere, Stability AI, and Amazon in further expanding choice and flexibility for customers. Amazon Bedrock is the first fully managed generative AI service to offer Llama 2, Meta’s next-generation LLM, through a managed API. Llama 2 models come with significant improvements over the original Llama models, including being trained on 40% more data and having a longer context length of 4,000 tokens to work with larger documents. Optimized to provide a fast response on AWS infrastructure, the Llama 2 models available via Amazon Bedrock are ideal for dialogue use cases. Customers can now build generative AI applications powered by Llama 2 13B and 70B parameter models, without the need to set up and manage any infrastructure. Amazon Titan FMs are a family of models created and pretrained by AWS on large datasets, making them powerful, general purpose capabilities built to support a variety of use cases. The first of these models generally available to customers, Amazon Titan Embeddings, is an LLM that converts text into numerical representations (known as embeddings) to power RAG use cases. FMs are well suited for a wide variety of tasks, but they can only respond to questions based on learnings from the training data and contextual information in a prompt, limiting their effectiveness when responses require timely knowledge or proprietary data. Data is the difference between a general generative AI application and one that truly knows your business and your customer. To augment FM responses with additional data, many organizations turn to RAG, a popular model-customization technique where an FM connects to a knowledge source that it can reference to augment its responses. To get started with RAG, customers first need access to an embedding model to convert their data into vectors that allow the FM to more easily understand the semantic meaning and relationships between data. Building an embeddings model requires massive amounts of data, resources, and ML expertise, putting RAG out of reach for many organizations. Amazon Titan Embeddings makes it easier for customers to get started with RAG to extend the power of any FM using their proprietary data. Amazon Titan Embeddings supports more than 25 languages and a context length of up to 8,192 tokens, making it well suited to work with single words, phrases, or entire documents based on the customer’s use case. The model returns output vectors of 1,536 dimensions, giving it a high degree of accuracy, while also optimizing for low-latency, cost-effective results. With new models and capabilities, it’s easy to use your organization’s data as a strategic asset to customize foundation models and build more differentiated experiences. Third, because the data customers want to use for customization is such valuable IP, they need it to remain secure and private. With security and privacy built in since day one, Amazon Bedrock customers can trust that their data remains protected. None of the customer’s data is used to train the original base FMs. All data is encrypted at rest and in transit. And you can expect the same AWS access controls that you have with any other AWS service. Today, we are excited to build on this foundation and introduce new security and governance capabilities – Amazon Bedrock is now a HIPAA eligible service and can be used in compliance with GDPR, allowing even more customers to benefit from generative AI. New governance capabilities include integration with Amazon CloudWatch to track usage metrics and build customized dashboards and integration with AWS CloudTrail to monitor API activity and troubleshoot issues. These new governance and security capabilities help organizations unlock the potential of generative AI, even in highly regulated industries, and ensure that data remains protected. Finally, certain periods of the year, like the holidays, are critical for customers to make sure their users can get uninterrupted service from applications powered by generative AI. During these periods, customers want to ensure their service is available to all of its customers regardless of the demand. Amazon Bedrock now allows customers to reserve throughput (in terms of tokens processed per minute) to maintain a consistent user experience even during peak traffic times. Together, the new capabilities and models we announced today for Amazon Bedrock will accelerate how quickly enterprises can build more personalized applications and enhance employee productivity. In concert with our ongoing investments in ML infrastructure, Amazon Bedrock is the best place for customers to build and scale generative AI applications. To help customers get started quickly with these new features, we are adding a new generative AI training for Amazon Bedrock to our collection of digital, on-demand training courses. Amazon Bedrock – Getting Started is a free, self-paced digital course that introduces learners to the service. This 60-minute course will introduce developers and technical audiences to Amazon Bedrock’s benefits, features, use cases, and technical concepts. Announcing Amazon CodeWhisperer customization capability to generate more relevant code recommendations informed by your organization’s code base At AWS, we are building powerful new applications that transform how our customers get work done with generative AI. In April 2023, we announced the general availability of Amazon CodeWhisperer, an AI coding companion that helps developers build software applications faster by providing code suggestions across 15 languages, based on natural language comments and code in a developer’s integrated developer environment (IDE). CodeWhisperer has been trained on billions of lines of publicly available code to help developers be more productive across a wide range of tasks. We have specially trained CodeWhisperer on high-quality Amazon code, including AWS APIs and best practices, to help developers be even faster and more accurate generating code that interacts with AWS services like Amazon Elastic Compute Cloud (Amazon EC2), Amazon Simple Storage Service (Amazon S3), and AWS Lambda. Customers from Accenture to Persistent to Bundesliga have been using CodeWhisperer to help make their developers more productive. Many customers also want CodeWhisperer to include their own internal APIs, libraries, best practices, and architectural patterns in its suggestions, so they can speed up development even more. Today, AI coding companions are not able to include these APIs in their code suggestions because they are typically trained on publicly available code, and so aren’t aware of a company’s internal code. For example, to build a feature for an ecommerce website that lists items in a shopping cart, developers have to find and understand existing internal code, such as the API that provides the description of items, so they can display the description in the shopping cart. Without a coding companion capable of suggesting the correct, internal code for them, developers have to spend hours digging through their internal code base and documentation to complete their work. Even after developers are able to find the right resources, they have to spend more time reviewing the code to make sure it follows their company’s best practices. Today, we are excited to announce a new Amazon CodeWhisperer customization capability, which enables CodeWhisperer to generate even better suggestions than before, because it can now include your internal APIs, libraries, best practices, and architectural patterns. This capability uses the latest model and context customization techniques and will be available in preview soon as part of a new CodeWhisperer Enterprise Tier. With this capability, you can securely connect your private repositories to CodeWhisperer, and with a few clicks, customize CodeWhisperer to generate real-time recommendations that include your internal code base. For example, with a CodeWhisperer customization, a developer working in a food delivery company can ask CodeWhisperer to provide recommendations that include specific code related to the company’s internal services, such as “Process a list of unassigned food deliveries around the driver’s current location.” Previously, CodeWhisperer would not know the correct internal APIs for “unassigned food deliveries” or “driver’s current location” because this isn’t publicly available information. Now, once customized on the company’s internal code base, CodeWhisperer understands the intent, determines which internal and public APIs are best suited to the task, and generates code recommendations for the developer. The CodeWhisperer customization capability can save developers hours spent searching and modifying sparsely documented code, and helps onboard developers who are new to the company faster. In the following example, after creating a private customization, AnyCompany (a food delivery company) developers get CodeWhisperer code recommendations that include their internal APIs and libraries. We conducted a recent study with Persistent, a global services and solutions company delivering digital engineering and enterprise modernization services to customers, to measure the productivity benefits of the CodeWhisperer customization capability. Persistent found that developers using the customization capability were able to complete their coding tasks up to 28% faster, on average, than developers using standard CodeWhisperer. We designed this customization capability with privacy and security at the forefront. Administrators can easily manage access to a private customization from the AWS Management Console, so that only specific developers have access. Administrators can also ensure that only repositories that meet their standards are eligible for use in a CodeWhisperer customization. Using high-quality repositories helps CodeWhisperer make suggestions that promote security and code quality best practices. Each customization is completely isolated from other customers and none of the customizations built with this new capability will be used to train the FM underlying CodeWhisperer, protecting customers’ valuable intellectual property. Announcing the preview of Generative BI authoring capabilities in Amazon QuickSight to help business analysts easily create and customize visuals using natural-language commands AWS has been on a mission to democratize access to insights for all users in the organization. Amazon QuickSight, our unified business intelligence (BI) service built for the cloud, allows insights to be shared across all users in the organization. With QuickSight, we’ve been using generative models to power Amazon QuickSight Q, which enable any user to ask questions of their data using natural language, without having to write SQL queries or learn a BI tool, since 2020. In July 2023, we announced that we are furthering the early innovation in QuickSight Q with the new LLM capabilities to provide Generative BI capabilities in QuickSight. Current QuickSight customers like BMW Group and Traeger Grills are looking forward to further increasing productivity of their analysts using the Generative BI authoring experience. Today, we are excited to make these LLM capabilities available in preview with Generative BI dashboard authoring capabilities for business analysts. The new Generative BI authoring capabilities extend the natural-language querying of QuickSight Q beyond answering well-structured questions (such as “what are the top 10 products sold in California?”) to help analysts quickly create customizable visuals from question fragments (such as “top 10 products”), clarify the intent of a query by asking follow-up questions, refine visualizations, and complete complex calculations. Business analysts simply describe the desired outcome, and QuickSight generates compelling visuals that can be easily added to a dashboard or report with a single click. QuickSight Q also offers related questions to help analysts clarify ambiguous cases when multiple data fields match their query. When the analyst has the initial visualization, they can add complex calculations, change chart types, and refine visuals using natural language prompts. The new Generative BI authoring capabilities in QuickSight Q make it fast and easy for business analysts to create compelling visuals and reduce the time to deliver the insights needed to inform data-driven decisions at scale. Creating visuals using Generative BI capabilities in Amazon QuickSight Creating visuals using Generative BI capabilities in Amazon QuickSight Generative AI tools and capabilities for every business Today’s announcements open generative AI up to any customer. With enterprise-grade security and privacy, choice of leading FMs, a data-first approach, and a highly performant, cost-effective infrastructure, organizations trust AWS to power their innovations with generative AI solutions at every layer of the stack. We have seen exciting innovation from Bridgewater Associates to Omnicom to Asurion to Rocket Mortgage, and with these new announcements, we look forward to new use cases and applications of the technology to boost productivity. This is just the beginning—across the technology stack, we are innovating with new services and capabilities built for your organization to help tackle some of your largest challenges and change how we work. Resources To learn more, check out the following resources: Explore generative AI on AWS Learn about Amazon Bedrock, the easiest way to build and scale generative AI applications with FMs Learn more about Llama2 on Amazon Bedrock Learn about Amazon Titan, high-performing FMs from Amazon to innovate responsibly Learn how you can use the Amazon CodeWhisperer customization capability Learn more about Generative BI features for QuickSight Discover generative AI solutions from AWS Partners in AWS Marketplace About the author Swami Sivasubramanian is Vice President of Data and Machine Learning at AWS. In this role, Swami oversees all AWS Database, Analytics, and AI & Machine Learning services. His team’s mission is to help organizations put their data to work with a complete, end-to-end data solution to store, access, analyze, and visualize, and predict. -------------------------------------------------------------------------------- /lab2/assets/streamlit/src/Home.py: -------------------------------------------------------------------------------- 1 | """ 2 | StreamLit app with the search engine UI: landing page 3 | """ 4 | 5 | ######################### 6 | # IMPORTS 7 | ######################### 8 | 9 | import logging 10 | import os 11 | import sys 12 | 13 | import streamlit as st 14 | from components.utils import display_cover_with_title, generate_qrcode, set_page_styling 15 | from dotenv import load_dotenv 16 | from PIL import Image 17 | from st_pages import show_pages_from_config 18 | from streamlit_extras.switch_page_button import switch_page 19 | 20 | # for local testing only 21 | if "COVER_IMAGE_URL" not in os.environ: 22 | load_dotenv() 23 | 24 | # Import required AFTER env loading 25 | import components.authenticate as authenticate 26 | from components.utils import add_logo 27 | import components.sns_api as sns_api 28 | 29 | LOGGER = logging.Logger("Home-Page", level=logging.DEBUG) 30 | HANDLER = logging.StreamHandler(sys.stdout) 31 | HANDLER.setFormatter(logging.Formatter("%(levelname)s | %(name)s | %(message)s")) 32 | LOGGER.addHandler(HANDLER) 33 | 34 | 35 | ######################### 36 | # CHECK LOGIN (do not delete) 37 | ######################### 38 | 39 | # check authentication 40 | authenticate.set_st_state_vars() 41 | 42 | 43 | ######################### 44 | # COVER & CONFIG 45 | ######################### 46 | 47 | # titles 48 | COVER_IMAGE = ( 49 | os.environ.get("COVER_IMAGE_URL") 50 | if "authenticated" in st.session_state and st.session_state["authenticated"] 51 | else os.environ.get("COVER_IMAGE_LOGIN_URL") 52 | ) 53 | TITLE = " Build & Scale GenAi Apps using Amazon Bedrock" 54 | DESCRIPTION = "Marketing Content Generation - Amazon Bedrock Workshop" 55 | PAGE_TITLE = "Amazon Bedrock Workshop" 56 | PAGE_ICON = ":ocean:" 57 | 58 | # page config 59 | st.set_page_config( 60 | page_title=PAGE_TITLE, 61 | page_icon=PAGE_ICON, 62 | layout="centered", 63 | initial_sidebar_state="expanded" 64 | if "authenticated" in st.session_state and st.session_state["authenticated"] 65 | else "collapsed", 66 | ) 67 | 68 | # page width, form borders, message styling 69 | set_page_styling() 70 | 71 | # display cover immediately so that it does not pop in and out on every page refresh 72 | cover_placeholder = st.empty() 73 | with cover_placeholder: 74 | display_cover_with_title( 75 | title=TITLE, 76 | description=DESCRIPTION, 77 | image_url=COVER_IMAGE, 78 | max_width="100%", 79 | ) 80 | 81 | # custom page names in the sidebar 82 | if "authenticated" in st.session_state and st.session_state["authenticated"]: 83 | show_pages_from_config() 84 | 85 | add_logo() 86 | 87 | ######################### 88 | # SIDEBAR 89 | ######################### 90 | 91 | # sidebar title 92 | if st.session_state["authenticated"]: 93 | st.sidebar.markdown( 94 | """ 95 | 107 | """, 108 | unsafe_allow_html=True, 109 | ) 110 | else: 111 | st.sidebar.markdown( 112 | """ 113 | 118 | """, 119 | unsafe_allow_html=True, 120 | ) 121 | 122 | ######################### 123 | # CONSTANTS 124 | ######################### 125 | 126 | GENERATED_QRCODES_PATH = "tmp/" 127 | 128 | 129 | ######################### 130 | # SESSION STATE VARIABLES 131 | ######################### 132 | 133 | st.session_state.setdefault("username", "") 134 | st.session_state.setdefault("password", "") 135 | st.session_state.setdefault("password_repeat", "") 136 | st.session_state.setdefault("new_password", "") 137 | st.session_state.setdefault("new_password_repeat", "") 138 | st.session_state.setdefault("email", "") 139 | st.session_state.setdefault("register", False) 140 | st.session_state.setdefault("account_created", False) 141 | st.session_state.setdefault("email_verified", False) 142 | 143 | 144 | ######################### 145 | # HELPER FUNCTIONS 146 | ######################### 147 | 148 | 149 | def subscribe_email() -> None: 150 | """ 151 | Subscribe email to SNS topic 152 | """ 153 | LOGGER.log(logging.DEBUG, ("Inside subscribe_email()")) 154 | if "user_email" in st.session_state and st.session_state["user_email"] != "": 155 | print(f"EMAIL: {st.session_state['user_email']}") 156 | print(f"TOKEN: {st.session_state['access_token']}") 157 | subscribe_success, message = sns_api.sns_topic_subscribe_email( 158 | st.session_state["user_email"], st.session_state["access_token"] 159 | ) 160 | if not subscribe_success: 161 | st.session_state["error_message"] = message 162 | else: 163 | st.session_state.pop("error_message", None) 164 | else: 165 | st.session_state["error_message"] = "Could not subscribe email." 166 | 167 | 168 | def toggle_register() -> None: 169 | """ 170 | Toggle register flag 171 | """ 172 | st.session_state["register"] = not st.session_state["register"] 173 | 174 | 175 | def create_account() -> None: 176 | """ 177 | Perform registration 178 | """ 179 | LOGGER.log(logging.DEBUG, ("Inside create_account()")) 180 | 181 | st.session_state["account_created"] = False 182 | st.session_state["email_verified"] = False 183 | 184 | if ( 185 | (st.session_state["username"] != "") 186 | & (st.session_state["password"] != "") 187 | & (st.session_state["password_repeat"] != "") 188 | & (st.session_state["email"] != "") 189 | ): 190 | if st.session_state["password"] == st.session_state["password_repeat"]: 191 | register_success, message = authenticate.create_user( 192 | st.session_state["username"], 193 | st.session_state["password"], 194 | st.session_state["email"], 195 | ) 196 | if not register_success: 197 | st.session_state["error_message"] = message 198 | else: 199 | st.session_state.pop("error_message", None) 200 | st.session_state["account_created"] = True 201 | st.session_state["user_to_verify"] = st.session_state["username"] 202 | else: 203 | st.session_state["error_message"] = "Entered passwords do not match." 204 | del st.session_state["password"] 205 | del st.session_state["password_repeat"] 206 | else: 207 | st.session_state[ 208 | "error_message" 209 | ] = "Please enter a username, an email and a password first." 210 | 211 | 212 | def verify_email() -> None: 213 | """ 214 | Verify email 215 | """ 216 | LOGGER.log(logging.DEBUG, ("Inside verify_email()")) 217 | 218 | if st.session_state["verification_code"] != "": 219 | email_verify_success, message = authenticate.confirm_sign_up( 220 | st.session_state["user_to_verify"], st.session_state["verification_code"] 221 | ) 222 | if not email_verify_success: 223 | st.session_state["error_message"] = message 224 | else: 225 | st.session_state.pop("error_message", None) 226 | st.session_state["email_verified"] = True 227 | del st.session_state["verification_code"] 228 | del st.session_state["user_to_verify"] 229 | toggle_register() 230 | else: 231 | st.session_state["error_message"] = "Please enter a code from your email first." 232 | 233 | 234 | def run_login() -> None: 235 | """ 236 | Perform login 237 | """ 238 | LOGGER.log(logging.DEBUG, ("Inside run_login()")) 239 | 240 | st.session_state["account_created"] = False 241 | st.session_state["email_verified"] = False 242 | 243 | # authenticate 244 | if (st.session_state["username"] != "") & (st.session_state["password"] != ""): 245 | authenticate.sign_in(st.session_state["username"], st.session_state["password"]) 246 | 247 | # check authentication 248 | if not st.session_state["authenticated"] and st.session_state[ 249 | "challenge" 250 | ] not in [ 251 | "NEW_PASSWORD_REQUIRED", 252 | "MFA_SETUP", 253 | "SOFTWARE_TOKEN_MFA", 254 | ]: 255 | st.session_state[ 256 | "error_message" 257 | ] = "Username or password are wrong. Please try again." 258 | else: 259 | st.session_state.pop("error_message", None) 260 | 261 | # ask to enter credentials 262 | else: 263 | st.session_state[ 264 | "error_message" 265 | ] = "Please enter a username and a password first." 266 | 267 | 268 | def reset_password() -> None: 269 | """ 270 | Reset password 271 | """ 272 | LOGGER.log(logging.DEBUG, ("Inside reset_password()")) 273 | 274 | if st.session_state["challenge"] == "NEW_PASSWORD_REQUIRED": 275 | if (st.session_state["new_password"] != "") & ( 276 | st.session_state["new_password_repeat"] != "" 277 | ): 278 | if ( 279 | st.session_state["new_password"] 280 | == st.session_state["new_password_repeat"] 281 | ): 282 | reset_success, message = authenticate.reset_password( 283 | st.session_state["new_password"] 284 | ) 285 | if not reset_success: 286 | st.session_state["error_message"] = message 287 | else: 288 | st.session_state.pop("error_message", None) 289 | else: 290 | st.session_state["error_message"] = "Entered passwords do not match." 291 | else: 292 | st.session_state["error_message"] = "Please enter a new password first." 293 | 294 | 295 | def setup_mfa() -> None: 296 | """ 297 | Setup MFA 298 | """ 299 | LOGGER.log(logging.DEBUG, ("Inside setup_mfa()")) 300 | 301 | if st.session_state["challenge"] == "MFA_SETUP": 302 | if st.session_state["mfa_verify_token"] != "": 303 | token_valid, message = authenticate.verify_token( 304 | st.session_state["mfa_verify_token"] 305 | ) 306 | if token_valid: 307 | mfa_setup_success, message = authenticate.setup_mfa() 308 | if not mfa_setup_success: 309 | st.session_state["error_message"] = message 310 | else: 311 | st.session_state.pop("error_message", None) 312 | else: 313 | st.session_state["error_message"] = message 314 | else: 315 | st.session_state[ 316 | "error_message" 317 | ] = "Please enter a code from your MFA app first." 318 | 319 | 320 | def sign_in_with_token() -> None: 321 | """ 322 | Verify MFA Code 323 | """ 324 | LOGGER.log(logging.DEBUG, ("Inside sign_in_with_token()")) 325 | 326 | if st.session_state["challenge"] == "SOFTWARE_TOKEN_MFA": 327 | if st.session_state["mfa_token"] != "": 328 | success, message = authenticate.sign_in_with_token( 329 | st.session_state["mfa_token"] 330 | ) 331 | if not success: 332 | st.session_state["error_message"] = message 333 | else: 334 | st.session_state.pop("error_message", None) 335 | else: 336 | st.session_state[ 337 | "error_message" 338 | ] = "Please enter a code from your MFA App first." 339 | 340 | 341 | ######################### 342 | # MAIN APP PAGE 343 | ######################### 344 | 345 | # page if authenticated 346 | if st.session_state["authenticated"]: 347 | st.markdown("") 348 | subscribe_email() 349 | st.info( 350 | """ 351 | ## Welcome - Build & Scale GenAi Apps using Amazon Bedrock Workshop 🚀 352 | """ 353 | ) 354 | _, col, _ = st.columns([2, 1, 2], gap="large") 355 | with col: 356 | if st.button("Get Started", type="primary"): 357 | switch_page("Customer List") 358 | 359 | # page if password needs to be reset 360 | elif st.session_state["challenge"] == "NEW_PASSWORD_REQUIRED": 361 | st.markdown("") 362 | st.warning("Please reset your password to use the app.") 363 | 364 | with st.form("password_reset_form"): 365 | # password input field 366 | new_password = st.text_input( 367 | key="new_password", 368 | placeholder="Enter your new password here", 369 | label="New Password", 370 | type="password", 371 | ) 372 | 373 | # password repeat input field 374 | new_password_repeat = st.text_input( 375 | key="new_password_repeat", 376 | placeholder="Please repeat the new password", 377 | label="Repeat New Password", 378 | type="password", 379 | ) 380 | 381 | # reset button 382 | reset_button = st.form_submit_button("Reset Password", on_click=reset_password) 383 | 384 | # page if user need to setup MFA 385 | elif st.session_state["challenge"] == "MFA_SETUP": 386 | st.markdown("") 387 | st.warning( 388 | "Scan the QR code with an MFA application such as [Authy](https://authy.com/) to access the app." 389 | ) 390 | 391 | # generate QR code 392 | with st.spinner("Generating QR Code..."): 393 | qrcode_path = generate_qrcode( 394 | url=str(st.session_state["mfa_setup_link"]), path=GENERATED_QRCODES_PATH 395 | ) 396 | 397 | # display QR code 398 | col1, col2, col3 = st.columns(3) 399 | with col1: 400 | st.write(" ") 401 | with col2: 402 | image = Image.open(qrcode_path) 403 | st.image(image, caption="MFA Setup QR Code") 404 | with col3: 405 | st.write(" ") 406 | 407 | # token input field 408 | with st.form("mfa_submit_form"): 409 | setup_mfa_token = st.text_input( 410 | key="mfa_verify_token", 411 | placeholder="Enter the verification code here", 412 | label="Verification Code", 413 | ) 414 | 415 | # submit button 416 | mfa_setup_button = st.form_submit_button("Verify Token", on_click=setup_mfa) 417 | 418 | # page if user needs to enter MFA token 419 | elif st.session_state["challenge"] == "SOFTWARE_TOKEN_MFA": 420 | st.markdown("") 421 | st.warning("Please provide a token from your MFA application.") 422 | 423 | # token input field 424 | with st.form("password_reset_form"): 425 | setup_mfa_token = st.text_input( 426 | key="mfa_token", 427 | placeholder="Enter the verification code here", 428 | label="Verification Token", 429 | ) 430 | 431 | # verification button 432 | mfa_submit_button = st.form_submit_button( 433 | "Verify Token", on_click=sign_in_with_token 434 | ) 435 | 436 | # page if user is logged out 437 | else: 438 | st.markdown("") 439 | st.warning("You are logged out, please log in.") 440 | 441 | if st.session_state["account_created"] and st.session_state["email_verified"]: 442 | st.success( 443 | f"User {st.session_state['username']} created successfuly!", icon="✅" 444 | ) 445 | 446 | # Show email verification page 447 | if ( 448 | st.session_state["register"] 449 | and st.session_state["account_created"] 450 | and not st.session_state["email_verified"] 451 | ): 452 | st.markdown("---") 453 | st.markdown("### Complete registration") 454 | st.markdown( 455 | "A verification code was sent to your email address. Please enter the code below to verify your email address." 456 | ) 457 | 458 | with st.form("text_input_form"): 459 | # verification code input field 460 | verification_code = st.text_input( 461 | key="verification_code", 462 | placeholder="Enter the verification code here", 463 | label="Verification Code", 464 | ) 465 | 466 | # verification button 467 | verification_button = st.form_submit_button( 468 | "Verify Email", on_click=verify_email 469 | ) 470 | # Show register page 471 | elif st.session_state["register"]: 472 | st.markdown("---") 473 | st.markdown("### Create new account") 474 | with st.form("text_input_form"): 475 | # username input field 476 | username = st.text_input( 477 | key="username", 478 | placeholder="Enter your username here", 479 | label="Username", 480 | ) 481 | 482 | email = st.text_input( 483 | key="email", 484 | placeholder="Enter your email here", 485 | label="Email", 486 | ) 487 | st.session_state["user_email"] = email 488 | 489 | # password input field 490 | password = st.text_input( 491 | key="password", 492 | placeholder="Enter your password here", 493 | label="Password", 494 | type="password", 495 | ) 496 | 497 | # password input field 498 | password_repeat = st.text_input( 499 | key="password_repeat", 500 | placeholder="Repeat your password here", 501 | label="Repeat Password", 502 | type="password", 503 | ) 504 | 505 | st.warning( 506 | "**Note:** Your password should contain uppercase and lowercase letters, numbers, and special characters.", 507 | icon="⚠️", 508 | ) 509 | 510 | # register button 511 | register_button = st.form_submit_button( 512 | "Create account", type="primary", on_click=create_account 513 | ) 514 | 515 | st.markdown("---") 516 | st.markdown("Already have an account?") 517 | login_button = st.button( 518 | "Log in here", type="secondary", on_click=toggle_register 519 | ) 520 | # Show Login page 521 | else: 522 | st.markdown("---") 523 | st.markdown("### Log in") 524 | with st.form("text_input_form"): 525 | # username input field 526 | username = st.text_input( 527 | key="username", 528 | placeholder="Enter your username here", 529 | label="Username", 530 | ) 531 | 532 | # password input field 533 | password = st.text_input( 534 | key="password", 535 | placeholder="Enter your password here", 536 | label="Password", 537 | type="password", 538 | ) 539 | 540 | # login button 541 | login_button = st.form_submit_button( 542 | "Log in", type="primary", on_click=run_login 543 | ) 544 | 545 | st.markdown("---") 546 | register_button = st.button( 547 | "Create new account", type="secondary", on_click=toggle_register 548 | ) 549 | 550 | 551 | # show error message 552 | if "error_message" in st.session_state: 553 | st.error(st.session_state["error_message"]) 554 | del st.session_state["error_message"] 555 | 556 | 557 | ######################### 558 | # FOOTNOTE 559 | ######################### 560 | 561 | # footnote 562 | 563 | st.text("") 564 | 565 | st.markdown("---") 566 | footer_col1, footer_col2 = st.columns(2) 567 | 568 | # log out button 569 | with footer_col1: 570 | st.button("Sign out", on_click=authenticate.sign_out) 571 | 572 | # copyright 573 | with footer_col2: 574 | st.markdown( 575 | "
© 2023 Amazon Web Services
", 576 | unsafe_allow_html=True, 577 | ) 578 | --------------------------------------------------------------------------------