├── .gitignore ├── 01-create-aurora-pgvector ├── .gitignore ├── README.md ├── app.py ├── cdk.json ├── check_aurora.png ├── create_aurora_pgvector │ ├── __init__.py │ └── create_aurora_pgvector_stack.py ├── lambdas │ ├── __init__.py │ ├── code │ │ └── table_creator │ │ │ ├── lambda_function.py │ │ │ └── pg_rds_api_help.py │ └── project_lambdas.py ├── layers │ ├── __init__.py │ ├── project_layers.py │ └── requests_aws_auth.zip ├── rds │ ├── __init__.py │ └── rds.py ├── requirements-dev.txt ├── requirements.txt ├── source.bat └── tests │ ├── __init__.py │ └── unit │ ├── __init__.py │ └── test_create_aurora_pgvector_stack.py ├── 02-create-bedrock-knowledge-bases ├── .gitignore ├── README.md ├── airline-qa-base │ └── PDF │ │ ├── Change_of_name_of_any_passenger_Lainventada_Airlines.pdf │ │ ├── Change_of_passage_from_round_from_flown_not_flown_lastainventada_airlines.pdf │ │ ├── How to know_if_can_change_the_passage_Lainventada_Airlines.pdf │ │ ├── Knows_the_the_right_to_retract_lainventada_airlines.pdf │ │ ├── Lainventada_airlines_ticket_cancellation_information.pdf │ │ ├── Request_the_refund_of_shipping_taxes_Lainventada_Airlines.pdf │ │ ├── buy_an_ticket_and_me_regretti_of_travel_Lainventada_Airlines.pdf │ │ ├── change passport_number_or_id_in_passage_lainventada_airlines.pdf │ │ ├── change_of_flights_or_dates_of_ticket_lainventada_airlines.pdf │ │ ├── change_of_passages_after_start_the_trip_the_travel_inventada_airlines.pdf │ │ ├── change_of_passages_before_an_affectation_LAINVENTADA_Airlines.pdf │ │ ├── change_of_passages_purchased_at_agencias_lainventada_airlines..pdf │ │ ├── deadline for_returns_of_passages_Lainventada_Airlines.pdf │ │ ├── deadlines for_make_an_change_of_passage_Lainventada_Airlines.pdf │ │ ├── discover_how_how_to receive_your_devolucin_lainventada_airlines.pdf │ │ ├── forward_or_postponingacin_flight_the same_from_Lainventada_airline.pdf │ │ ├── get_what_do_in_case_of_fraud_Lainventada_Airlines.pdf │ │ ├── how_to_request_the_devolucin_from_your_ticket_Lainventada_Airlines.pdf │ │ ├── refunds_of_money_for_passage_launventada_airlines.pdf │ │ ├── return_for_reduction_of_shipment_taxes_lastainventada_airlines.pdf │ │ ├── where to check_cost_of_change_of_passages_lainventada_Airlines.pdf │ │ ├── where_can_change_of_passages_Lainventada_Airlines.pdf │ │ └── where_to_request_the_return_of_the Lainventada_Airlines ticket.pdf ├── app.py ├── cdk.json ├── create_bedrock_knowledge_bases │ ├── __init__.py │ └── create_bedrock_knowledge_bases_stack.py ├── data_source.png ├── get_param.py ├── kb_role │ ├── __init__.py │ └── create_role.py ├── knowledge_base │ ├── __init__.py │ ├── datasource.py │ └── knowledgebase.py ├── requirements-dev.txt ├── requirements.txt ├── s3_cloudfront │ ├── __init__.py │ └── s3_cloudfront_website.py ├── source.bat ├── test_kb.png └── tests │ ├── __init__.py │ └── unit │ ├── __init__.py │ └── test_create_bedrock_knowledge_bases_stack.py ├── 03-rag-agent-bedrock ├── .gitignore ├── 03_agent.jpg ├── README.md ├── agent_bedrock │ ├── __init__.py │ └── create_agent_with_kb_ag.py ├── agent_role │ ├── __init__.py │ └── create_role.py ├── app.py ├── cdk.json ├── databases │ ├── __init__.py │ └── databases.py ├── get_param.py ├── lambdas │ ├── __init__.py │ ├── code │ │ ├── ask_date │ │ │ └── lambda_function.py │ │ ├── dynamodb_put_item_from_csv │ │ │ ├── dataset.csv │ │ │ └── lambda_function.py │ │ ├── dynamodb_put_item_random_key │ │ │ └── lambda_function.py │ │ └── dynamodb_query │ │ │ └── lambda_function.py │ ├── datasource.py │ └── project_lambdas.py ├── layers │ ├── __init__.py │ ├── aiofile-amazon-transcribe.zip │ ├── bs4_requests.zip │ ├── common │ │ └── python │ │ │ ├── agent_utils.py │ │ │ ├── db_utils.py │ │ │ ├── file_utils.py │ │ │ └── utils.py │ └── project_layers.py ├── rag_agent_bedrock │ ├── __init__.py │ ├── ag_data.json │ ├── agent_data.json │ ├── agent_prompt.txt │ ├── kb_data.json │ ├── rag_agent_bedrock_stack.py │ └── utils_agent.py ├── requirements-dev.txt ├── requirements.txt ├── reschedule_flight.gif ├── source.bat ├── tests │ ├── __init__.py │ └── unit │ │ ├── __init__.py │ │ └── test_rag_agent_bedrock_stack.py └── ticket.gif ├── 04-whatsapp-app ├── .gitignore ├── README.md ├── apis │ ├── __init__.py │ └── webhooks.py ├── app.py ├── cdk.json ├── databases │ ├── __init__.py │ └── databases.py ├── get_param.py ├── imagenes │ ├── 1_step.jpg │ ├── 2_1_step.jpg │ ├── 2_2_step.jpg │ ├── 2_step.jpg │ ├── 3_step.jpg │ ├── QA.gif │ ├── arquitectura.jpg │ ├── deployment_time.jpg │ ├── flow.jpg │ ├── gif_01.gif │ ├── invoke_url.jpg │ ├── passanger_information.gif │ ├── secret.png │ ├── voice_note.gif │ ├── webhook.png │ └── whaytsapp_number.jpg ├── lambdas │ ├── __init__.py │ ├── code │ │ ├── agent_bedrock │ │ │ └── lambda_function.py │ │ ├── audio_job_transcriptor │ │ │ └── lambda_function.py │ │ ├── process_stream │ │ │ └── lambda_function.py │ │ ├── transcriber_done │ │ │ └── lambda_function.py │ │ ├── whatsapp_in │ │ │ └── lambda_function.py │ │ └── whatsapp_out │ │ │ └── lambda_function.py │ └── project_lambdas.py ├── layers │ ├── __init__.py │ ├── aiofile-amazon-transcribe.zip │ ├── bs4_requests.zip │ ├── common │ │ └── python │ │ │ ├── agent_utils.py │ │ │ ├── db_utils.py │ │ │ ├── file_utils.py │ │ │ └── utils.py │ └── project_layers.py ├── requirements-dev.txt ├── requirements.txt ├── s3_cloudfront │ ├── __init__.py │ ├── s3_cloudfront │ │ ├── __init__.py │ │ └── s3_cloudfront_website.py │ └── s3_cloudfront_website.py ├── source.bat ├── tests │ ├── __init__.py │ └── unit │ │ ├── __init__.py │ │ └── test_whatsapp_app_stack.py ├── text-to-whatsapp │ └── test.txt └── whatsapp_app │ ├── __init__.py │ └── whatsapp_app_stack.py ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md └── imagen ├── check_aurora.png ├── diagram.png ├── diagram_1.jpg ├── diagram_2.png └── part_1.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | 164 | .vscode 165 | .DS_Store -------------------------------------------------------------------------------- /01-create-aurora-pgvector/.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | package-lock.json 3 | __pycache__ 4 | .pytest_cache 5 | .venv 6 | *.egg-info 7 | 8 | # CDK asset staging directory 9 | .cdk.staging 10 | cdk.out 11 | -------------------------------------------------------------------------------- /01-create-aurora-pgvector/README.md: -------------------------------------------------------------------------------- 1 | # Part 1: Building an Amazon Aurora PostgreSQL vector database as a Knowledge Base for Amazon Bedrock. 2 | 3 | Welcome to the first installment of our four-part series on creating a WhatsApp-powered RAG Travel Support Agent. In this part, we'll dive into the foundational step of our architecture: setting up an Amazon Aurora PostgreSQL vector database. This database will serve as the backbone of our knowledge base for Amazon Bedrock, enabling fast and efficient retrieval of information for our AI-powered travel assistant. 4 | 5 | ## The Importance of Vector Databases 6 | 7 | Vector databases are crucial for implementing Retrieval Augmented Generation (RAG) systems, as they allow for semantic search capabilities that go beyond traditional keyword matching. By using Amazon Aurora PostgreSQL with vector support, we're leveraging a powerful, scalable, and fully managed database service that's optimized for AI and machine learning workloads. 8 | 9 | ## Overview of the Setup Process 10 | 11 | In this part, we'll use [AWS Cloud Development Kit (CDK)](https://aws.amazon.com/cdk) for Python to: 12 | 13 | 1. Set up an Amazon Aurora PostgreSQL Serverless v2 database cluster 14 | 2. Create a database secret 15 | 3. Initialize a custom resource to set up a PostgreSQL table 16 | 4. Grant necessary permissions to the custom resource 17 | 5. Store key information in the [AWS Systems Manager (SSM) Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html). 18 | 19 | Once completed, our Aurora PostgreSQL database will be ready to serve as a Knowledge Base for Amazon Bedrock. 20 | 21 | ## Detailed Preparation Steps 22 | 23 | Our setup process, automated through an AWS Lambda function via a custom resource in CDK, involves the following key steps: 24 | 25 | ![Digrama parte 1](/imagen/part_1.jpg) 26 | 27 | The preparation consists of the following steps: 28 | 29 | 1- Install the pgvector extension (version 0.5.0 or higher): 30 | 31 | ```sql 32 | CREATE EXTENSION IF NOT EXISTS vector; 33 | SELECT extversion FROM pg_extension WHERE extname='vector'; 34 | ``` 35 | This enables vector storage and HNSW indexing, crucial for efficient similarity searches. 36 | 37 | 2- Create a dedicated schema and user role: 38 | 39 | ```sql 40 | CREATE SCHEMA bedrock_integration; 41 | CREATE ROLE bedrock_user WITH PASSWORD password LOGIN; 42 | ``` 43 | This segregates our Bedrock-related data and provides controlled access. 44 | 45 | 3. Grant permissions to the bedrock_user: 46 | ```sql 47 | GRANT ALL ON SCHEMA bedrock_integration to bedrock_user; 48 | ``` 49 | This allows the user to manage the schema, including creating tables and indexes. 50 | 51 | 52 | 4. Create the knowledge base table: 53 | ```sql 54 | CREATE TABLE IF NOT EXISTS bedrock_integration.bedrock_kb ( 55 | id uuid PRIMARY KEY, 56 | embedding vector(1024), 57 | chunks text, 58 | metadata json 59 | ); 60 | ``` 61 | This table structure accommodates vector embeddings, text chunks, and associated metadata. You should use vector length [according to the embedding model's](https://docs.aws.amazon.com/bedrock/latest/userguide/knowledge-base-supported.html) output dimension. Here we are using 1024 which is compatible with Amazon Titan Embeddings V2 and Cohere Embeddings. 62 | 63 | If you want to leverage [document metadata for filtering ](https://docs.aws.amazon.com/bedrock/latest/userguide/knowledge-base-ds.html#kb-ds-metadata) you need to add those fields as columns. For example, if you need to filter by topic or language, add those columns in advance: 64 | 65 | ```sql 66 | CREATE TABLE IF NOT EXISTS bedrock_integration.bedrock_kb ( 67 | id uuid PRIMARY KEY, 68 | embedding vector(1024), 69 | chunks text, metadata json, 70 | topic text, language varchar(10) 71 | ); 72 | ``` 73 | 74 | 75 | 5. Create an index for efficient similarity searches: 76 | ```sql 77 | CREATE INDEX on bedrock_integration.bedrock_kb USING hnsw (embedding vector_cosine_ops); 78 | ``` 79 | This HNSW (Hierarchical Navigable Small World) index optimizes cosine similarity searches. 80 | 81 | ## Building the Infrastructure 82 | 83 | ✅ To set up this infrastructure: 84 | 85 | 1. Navigate to the project directory: 86 | ``` 87 | cd 01-create-aurora-pgvector 88 | ``` 89 | 90 | 2. Create and activate a virtual environment: 91 | ``` 92 | python3 -m venv .venv 93 | source .venv/bin/activate 94 | ``` 95 | For Windows: 96 | ``` 97 | .venv\Scripts\activate.bat 98 | ``` 99 | 100 | >[CDK Guide](https://docs.aws.amazon.com/cdk/v2/guide/hello_world.html) 101 | 102 | 3. Install the required dependencies: 103 | ``` 104 | pip install -r requirements.txt 105 | ``` 106 | 107 | 4. Deploy the CDK stack (this may take some time): 108 | ``` 109 | cdk deploy 110 | ``` 111 | 112 | You can monitor the deployment progress in the [AWS CloudFormation console](https://console.aws.amazon.com/cloudformation). 113 | 114 | 115 | ### About the VPC 116 | 117 | This stack creates a new VPC, be aware of reaching [service limits](https://docs.aws.amazon.com/vpc/latest/userguide/amazon-vpc-limits.html) and adjust in advance. Also, since the app uses [RDS Data API](https://docs.aws.amazon.com/rdsdataservice/latest/APIReference/API_ExecuteStatement.html), it doesn't need internet access (nat_gateways = 0) 118 | 119 | 120 | ```python 121 | # Crear una VPC 122 | self.vpc = ec2.Vpc(self, "VPC", max_azs=2, nat_gateways=0) 123 | ``` 124 | You can use an existing VPC changing the CDK code. 125 | 126 | ### Take a look at the fresh Aurora Serverless Cluster in the RDS Console 127 | 128 | Go to the [RDS Query Editor](https://us-east-1.console.aws.amazon.com/rds/home?region=us-east-1#query-editor:) and access your cluster (using the Database Secret ARN: bedrockSecretXXX) 129 | 130 | Execute `select * from bedrock_integration.knowledge_bases limit 5;` to see the empty table that you just created. 131 | 132 | ![alt text](check_aurora.png) 133 | 134 | ### Security best practice 135 | 136 | Database credentials are stored as Secrets in [AWS Secrets Manager](https://aws.amazon.com/secrets-manager) which is a service to centrally and securely store and access credentials. Never store credentials in your code or environment variables. 137 | 138 | 139 | ## 💰 Associated Costs 140 | 141 | Be aware of the costs associated with the following AWS services: 142 | 143 | - [Amazon Aurora Pricing](https://aws.amazon.com/rds/aurora/pricing/) 144 | - [Amazon Lambda Pricing](https://aws.amazon.com/lambda/pricing/) 145 | - [AWS Systems Manager pricing](https://aws.amazon.com/systems-manager/pricing/) 146 | 147 | > 👾 Note: You can custom the Aurora PostgreSQL Serverless v2 database cluster [settings](https://github.com/build-on-aws/rag-postgresql-agent-bedrock/blob/main/01-create-aurora-pgvector/rds/rds.py). For more information, refer to the [Requirements and limitations for Aurora Serverless v2](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-serverless-v2.requirements.html). 148 | 149 | ## Next Steps 150 | 151 | With your Amazon Aurora PostgreSQL vector database now set up, you're ready to move on to the next part of our series, where we'll create a Knowledge Base for Amazon Bedrock using this database. This will bring us one step closer to our goal of building a sophisticated, AI-powered travel support agent. -------------------------------------------------------------------------------- /01-create-aurora-pgvector/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | 4 | import aws_cdk as cdk 5 | 6 | from create_aurora_pgvector.create_aurora_pgvector_stack import CreateAuroraPgvectorStack 7 | 8 | 9 | app = cdk.App() 10 | CreateAuroraPgvectorStack(app, "Aurora-Pgvector", 11 | # If you don't specify 'env', this stack will be environment-agnostic. 12 | # Account/Region-dependent features and context lookups will not work, 13 | # but a single synthesized template can be deployed anywhere. 14 | 15 | # Uncomment the next line to specialize this stack for the AWS Account 16 | # and Region that are implied by the current CLI configuration. 17 | 18 | #env=cdk.Environment(account=os.getenv('CDK_DEFAULT_ACCOUNT'), region=os.getenv('CDK_DEFAULT_REGION')), 19 | 20 | # Uncomment the next line if you know exactly what Account and Region you 21 | # want to deploy the stack to. */ 22 | 23 | #env=cdk.Environment(account='123456789012', region='us-east-1'), 24 | 25 | # For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html 26 | ) 27 | 28 | app.synth() 29 | -------------------------------------------------------------------------------- /01-create-aurora-pgvector/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "python3 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 | "**/__pycache__", 14 | "tests" 15 | ] 16 | }, 17 | "context": { 18 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 19 | "@aws-cdk/core:checkSecretUsage": true, 20 | "@aws-cdk/core:target-partitions": [ 21 | "aws", 22 | "aws-cn" 23 | ], 24 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 25 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 26 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 27 | "@aws-cdk/aws-iam:minimizePolicies": true, 28 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 29 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 30 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 31 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 32 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 33 | "@aws-cdk/core:enablePartitionLiterals": true, 34 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 35 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 36 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 37 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 38 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 39 | "@aws-cdk/aws-route53-patters:useCertificate": true, 40 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 41 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 42 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 43 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 44 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 45 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 46 | "@aws-cdk/aws-redshift:columnId": true, 47 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 48 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 49 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 50 | "@aws-cdk/aws-kms:aliasNameRef": true, 51 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 52 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 53 | "@aws-cdk/aws-efs:denyAnonymousAccess": true, 54 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, 55 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, 56 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, 57 | "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, 58 | "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, 59 | "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, 60 | "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, 61 | "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, 62 | "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, 63 | "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, 64 | "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, 65 | "@aws-cdk/aws-eks:nodegroupNameAttribute": true, 66 | "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /01-create-aurora-pgvector/check_aurora.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/01-create-aurora-pgvector/check_aurora.png -------------------------------------------------------------------------------- /01-create-aurora-pgvector/create_aurora_pgvector/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/01-create-aurora-pgvector/create_aurora_pgvector/__init__.py -------------------------------------------------------------------------------- /01-create-aurora-pgvector/create_aurora_pgvector/create_aurora_pgvector_stack.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | Stack, 3 | CustomResource, 4 | aws_iam as iam, 5 | aws_ssm as ssm, 6 | aws_rds as rds 7 | ) 8 | import json 9 | 10 | from constructs import Construct 11 | from rds import AuroraDatabaseCluster 12 | from lambdas import Lambdas 13 | 14 | class CreateAuroraPgvectorStack(Stack): 15 | 16 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 17 | super().__init__(scope, construct_id, **kwargs) 18 | default_database_name = "kbdata" 19 | bedrock_user = "bedrock_user" 20 | table_name = 'knowledge_bases' 21 | 22 | aur = AuroraDatabaseCluster(self, "AuroraDatabaseCluster", default_database_name) 23 | 24 | bedrock_secret = rds.DatabaseSecret(self, "bedrockSecret", username=bedrock_user, exclude_characters=" ,%+~`#$&*()|[]{}:;<>?!'/@\".=") 25 | Fn = Lambdas(self, "L") 26 | 27 | 28 | pg_setup = CustomResource( self, 29 | "pg_setup", 30 | resource_type="Custom::PGSetup", 31 | service_token=Fn.table_creator.function_arn, 32 | properties=dict( 33 | cluster_arn=aur.cluster.cluster_arn, 34 | secrets_arn= aur.cluster.secret.secret_arn, 35 | table_name=table_name, 36 | database_name=default_database_name, 37 | credentials_arn = bedrock_secret.secret_arn, 38 | ) 39 | ) 40 | pg_setup.node.add_dependency(aur.cluster) 41 | pg_setup.node.add_dependency(bedrock_secret) 42 | 43 | 44 | Fn.table_creator.add_to_role_policy(iam.PolicyStatement(actions=["rds-data:ExecuteStatement"], 45 | resources=[aur.cluster.cluster_arn])) 46 | Fn.table_creator.add_to_role_policy(iam.PolicyStatement(actions=["secretsmanager:GetSecretValue"], 47 | resources=[aur.cluster.secret.secret_arn])) 48 | Fn.table_creator.add_to_role_policy(iam.PolicyStatement(actions=["secretsmanager:GetSecretValue"], 49 | resources=[bedrock_secret.secret_arn])) 50 | 51 | ssm.StringParameter( self, "cluster_arn_ssm", parameter_name=f"/pgvector/cluster_arn", string_value=aur.cluster.cluster_arn) 52 | ssm.StringParameter( self, "secret_arn_ssm", parameter_name=f"/pgvector/secret_arn", string_value=bedrock_secret.secret_arn) 53 | ssm.StringParameter( self, "table_ssm", parameter_name=f"/pgvector/table_name", string_value=table_name) 54 | -------------------------------------------------------------------------------- /01-create-aurora-pgvector/lambdas/__init__.py: -------------------------------------------------------------------------------- 1 | from lambdas.project_lambdas import Lambdas -------------------------------------------------------------------------------- /01-create-aurora-pgvector/lambdas/code/table_creator/lambda_function.py: -------------------------------------------------------------------------------- 1 | '''Custom generic CloudFormation resource example''' 2 | 3 | import json 4 | import requests 5 | import boto3 6 | from pg_rds_api_help import PGSetup 7 | 8 | 9 | client = boto3.client('rds-data') 10 | 11 | def lambda_handler(event, context): 12 | '''Handle Lambda event from AWS''' 13 | # Setup alarm for remaining runtime minus a second 14 | # signal.alarm((context.get_remaining_time_in_millis() / 1000) - 1) 15 | try: 16 | 17 | print('REQUEST RECEIVED:', event) 18 | print('REQUEST RECEIVED:', context) 19 | if event['RequestType'] == 'Create': 20 | print('CREATE!') 21 | event['PhysicalResourceId'] = 'NOT_YET' 22 | create(event, context) 23 | elif event['RequestType'] == 'Update': 24 | print('UPDATE!') 25 | create(event, context) 26 | 27 | elif event['RequestType'] == 'Delete': 28 | print('DELETE!') 29 | delete(event, context) 30 | 31 | else: 32 | print('FAILED!') 33 | send_response(event, context, "FAILED", 34 | {"Message": "Unexpected event received from CloudFormation"}) 35 | except Exception as error: 36 | print('FAILED!', error) 37 | send_response(event, context, "FAILED", { 38 | "Message": "Exception during processing"}) 39 | 40 | 41 | def create(event, context): 42 | 43 | if "ResourceProperties" in event: 44 | print ("create_datasource") 45 | props = event['ResourceProperties'] 46 | cluster_arn=props['cluster_arn'] 47 | table_name = props['table_name'] 48 | database_name=props['database_name'] 49 | secrets_arn = props['secrets_arn'] 50 | credentials_arn = props['credentials_arn'] 51 | 52 | PG = PGSetup( 53 | client=client, 54 | cluster_arn=cluster_arn, 55 | secrets_arn=secrets_arn, 56 | database_name=database_name, 57 | table_name = table_name, 58 | credentials_arn= credentials_arn 59 | ) 60 | 61 | PG.setup() 62 | 63 | event['PhysicalResourceId'] = f"{table_name}|SETUP" 64 | send_response(event, context, "SUCCESS",{"Message": "Resource creation successful!"}) 65 | else: 66 | print("no resource properties!") 67 | 68 | 69 | def delete (event, context): 70 | if 'PhysicalResourceId' in event: 71 | send_response(event, context, "SUCCESS", {"Message": "Resource deletion successful!"}) 72 | 73 | 74 | def send_response(event, context, response_status, response_data): 75 | '''Send a resource manipulation status response to CloudFormation''' 76 | response_body = json.dumps({ 77 | "Status": response_status, 78 | "Reason": "See the details in CloudWatch Log Stream: " + context.log_stream_name, 79 | "PhysicalResourceId": event['PhysicalResourceId'] if 'PhysicalResourceId' in event else "NOPHYID", 80 | "StackId": event['StackId'], 81 | "RequestId": event['RequestId'], 82 | "LogicalResourceId": event['LogicalResourceId'], 83 | "Data": response_data 84 | }) 85 | headers = { 86 | 'Content-Type': 'application/json', 87 | 'Content-Length': str(len(response_body)) 88 | } 89 | 90 | 91 | print('ResponseURL: ', event['ResponseURL']) 92 | print('ResponseBody:', response_body) 93 | 94 | response = requests.put(event['ResponseURL'], 95 | data=response_body, headers=headers) 96 | 97 | print("Status code:", response.status_code) 98 | print("Status message:", response.text) 99 | 100 | return response 101 | 102 | 103 | def timeout_handler(_signal, _frame): 104 | '''Handle SIGALRM''' 105 | raise Exception('Time exceeded') 106 | 107 | # signal.signal(signal.SIGALRM, timeout_handler) -------------------------------------------------------------------------------- /01-create-aurora-pgvector/lambdas/code/table_creator/pg_rds_api_help.py: -------------------------------------------------------------------------------- 1 | 2 | import boto3 3 | import json 4 | class PGSetup(): 5 | def __init__(self, client, cluster_arn, secrets_arn, database_name, table_name,credentials_arn): 6 | self.cluster_arn = cluster_arn 7 | self.secrets_arn = secrets_arn 8 | self.credentials_arn = credentials_arn 9 | self.database_name = database_name 10 | self.table_name = table_name 11 | 12 | self.client = client 13 | 14 | response = boto3.client('secretsmanager').get_secret_value(SecretId=credentials_arn) 15 | creds = json.loads(response.get("SecretString")) 16 | self.user = creds.get("username") 17 | self.user_password = creds.get("password") 18 | 19 | def setup(self): 20 | self.create_extension_vector() 21 | self.create_schema() 22 | self.create_role() 23 | self.grant_privileges() 24 | self.create_tables() 25 | 26 | def create_tables(self): 27 | table_name = self.sanitize_table_name(self.table_name) 28 | sql = f"CREATE TABLE IF NOT EXISTS bedrock_integration.{table_name} (id uuid PRIMARY KEY, embedding vector(1024), chunks text, metadata json, \"date\" text, source text, topic text, language varchar(10));" 29 | 30 | response = self.client.execute_statement( 31 | resourceArn=self.cluster_arn, 32 | secretArn=self.credentials_arn, 33 | sql=sql, 34 | database=self.database_name, 35 | formatRecordsAs='JSON' 36 | ) 37 | del response['ResponseMetadata'] 38 | print(f"CREATE TABLE : {response}") 39 | 40 | 41 | response2 = self.client.execute_statement( 42 | resourceArn=self.cluster_arn, 43 | secretArn=self.credentials_arn, 44 | sql=f"CREATE INDEX on bedrock_integration.{table_name} USING hnsw (embedding vector_cosine_ops)", 45 | database=self.database_name, 46 | formatRecordsAs='JSON' 47 | ) 48 | del response2['ResponseMetadata'] 49 | print(f"CREATE INDEX : {response2}") 50 | 51 | return response2 52 | 53 | 54 | def grant_privileges(self): 55 | sql = f'GRANT ALL ON SCHEMA bedrock_integration to {self.user}' 56 | print(f"{sql} :", end="") 57 | response = self.client.execute_statement( 58 | resourceArn=self.cluster_arn, 59 | secretArn=self.secrets_arn, 60 | sql=sql, 61 | database=self.database_name, 62 | formatRecordsAs='JSON' 63 | ) 64 | del response['ResponseMetadata'] 65 | print (response) 66 | return response 67 | 68 | 69 | def create_role(self): 70 | try: 71 | response = self.client.execute_statement( 72 | resourceArn=self.cluster_arn, 73 | secretArn=self.secrets_arn, 74 | sql=f"CREATE ROLE {self.user} LOGIN PASSWORD '{self.user_password}'", 75 | database=self.database_name, 76 | formatRecordsAs='JSON' 77 | ) 78 | del response['ResponseMetadata'] 79 | print(f"CREATE ROLE bedrock_user : {response}") 80 | return response 81 | except self.client.exceptions.DatabaseErrorException as e: 82 | print(f"CREATE ROLE bedrock_user : {e}") 83 | return e 84 | 85 | def create_schema(self): 86 | sql = 'CREATE SCHEMA IF NOT EXISTS bedrock_integration' 87 | response = self.client.execute_statement( 88 | resourceArn=self.cluster_arn, 89 | secretArn=self.secrets_arn, 90 | sql=sql, 91 | database=self.database_name, 92 | formatRecordsAs='JSON' 93 | ) 94 | del response['ResponseMetadata'] 95 | print(f"{sql} : {response}") 96 | return response 97 | 98 | def create_extension_vector(self): 99 | sql = 'CREATE EXTENSION IF NOT EXISTS vector' 100 | response = self.client.execute_statement( 101 | resourceArn=self.cluster_arn, 102 | secretArn=self.secrets_arn, 103 | sql=sql, 104 | database=self.database_name, 105 | formatRecordsAs='JSON' 106 | ) 107 | del response['ResponseMetadata'] 108 | print(f"{sql} : {response}") 109 | return response 110 | 111 | 112 | 113 | """ 114 | select id, chunks, metadata, "date", source, topic, language from bedrock_integration.knowledge_bases where topic is null limit 5; 115 | drop table bedrock_integration.knowledge_bases; 116 | 117 | 118 | CREATE TABLE IF NOT EXISTS bedrock_integration.knowledge_bases (id uuid PRIMARY KEY, embedding vector(1024), chunks text, metadata json, "date" text, source text, topic text, language varchar(10)); 119 | 120 | CREATE INDEX on bedrock_integration.knowledge_bases USING hnsw (embedding vector_cosine_ops); 121 | 122 | SELECT column_name, data_type, character_maximum_length FROM information_schema. columns WHERE table_name = 'knowledge_bases'; 123 | 124 | """ 125 | -------------------------------------------------------------------------------- /01-create-aurora-pgvector/lambdas/project_lambdas.py: -------------------------------------------------------------------------------- 1 | 2 | from aws_cdk import ( 3 | Duration, 4 | aws_iam as iam, 5 | aws_lambda, 6 | 7 | ) 8 | 9 | from constructs import Construct 10 | from layers import RequestsAWSAuth 11 | LAMBDA_TIMEOUT= 900 12 | 13 | BASE_LAMBDA_CONFIG = dict ( 14 | timeout=Duration.seconds(LAMBDA_TIMEOUT), 15 | architecture=aws_lambda.Architecture.ARM_64, 16 | runtime=aws_lambda.Runtime.PYTHON_3_12, 17 | tracing= aws_lambda.Tracing.ACTIVE) 18 | 19 | 20 | class Lambdas(Construct): 21 | 22 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 23 | super().__init__(scope, construct_id, **kwargs) 24 | 25 | requests = RequestsAWSAuth(self, "R") 26 | 27 | self.table_creator = aws_lambda.Function( 28 | self, 29 | "table_creator", 30 | layers=[requests.layer], 31 | handler="lambda_function.lambda_handler", 32 | code=aws_lambda.Code.from_asset("./lambdas/code/table_creator"), 33 | **BASE_LAMBDA_CONFIG 34 | ) 35 | 36 | 37 | 38 | self.table_creator.add_to_role_policy(iam.PolicyStatement(actions=["iam:PassRole"], resources=["*"])) -------------------------------------------------------------------------------- /01-create-aurora-pgvector/layers/__init__.py: -------------------------------------------------------------------------------- 1 | from layers.project_layers import RequestsAWSAuth -------------------------------------------------------------------------------- /01-create-aurora-pgvector/layers/project_layers.py: -------------------------------------------------------------------------------- 1 | import json 2 | from constructs import Construct 3 | 4 | from aws_cdk import ( 5 | aws_lambda as _lambda 6 | ) 7 | 8 | 9 | 10 | class RequestsAWSAuth(Construct): 11 | 12 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 13 | super().__init__(scope, construct_id, **kwargs) 14 | 15 | req_aws_auth = _lambda.LayerVersion( 16 | self, "RequestsAuth", code=_lambda.Code.from_asset("./layers/requests_aws_auth.zip"), 17 | compatible_runtimes = [_lambda.Runtime.PYTHON_3_9, _lambda.Runtime.PYTHON_3_10, _lambda.Runtime.PYTHON_3_11, _lambda.Runtime.PYTHON_3_12], 18 | description = 'Requests+aws_auth') 19 | self.layer = req_aws_auth -------------------------------------------------------------------------------- /01-create-aurora-pgvector/layers/requests_aws_auth.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/01-create-aurora-pgvector/layers/requests_aws_auth.zip -------------------------------------------------------------------------------- /01-create-aurora-pgvector/rds/__init__.py: -------------------------------------------------------------------------------- 1 | from rds.rds import AuroraDatabaseCluster -------------------------------------------------------------------------------- /01-create-aurora-pgvector/rds/rds.py: -------------------------------------------------------------------------------- 1 | 2 | from aws_cdk import ( 3 | aws_rds as rds, 4 | aws_ec2 as ec2, 5 | RemovalPolicy, 6 | ) 7 | from constructs import Construct 8 | 9 | 10 | class AuroraDatabaseCluster(Construct): 11 | def __init__(self, scope: Construct, construct_id: str, default_database_name,acu = 0.5, **kwargs) -> None: 12 | super().__init__(scope, construct_id, **kwargs) 13 | 14 | # Crear una VPC 15 | self.vpc = ec2.Vpc(self, "VPC", max_azs=2, nat_gateways=0) 16 | 17 | # Crear un grupo de seguridad para la base de datos 18 | self.security_group = ec2.SecurityGroup( 19 | self, "SecurityGroup", 20 | vpc=self.vpc, 21 | allow_all_outbound=True 22 | ) 23 | 24 | # Crear un cluster de base de datos Aurora PostgreSQL Serveless V2 25 | self.cluster = rds.DatabaseCluster( 26 | self, "AuroraPostgreSQLCluster", 27 | default_database_name= default_database_name, 28 | engine=rds.DatabaseClusterEngine.aurora_postgres(version=rds.AuroraPostgresEngineVersion.VER_16_2), 29 | credentials=rds.Credentials.from_generated_secret("clusteradmin"), 30 | writer=rds.ClusterInstance.serverless_v2("writer"), 31 | serverless_v2_min_capacity=acu, 32 | serverless_v2_max_capacity=acu*2, 33 | vpc_subnets=ec2.SubnetSelection( 34 | subnet_type=ec2.SubnetType.PRIVATE_ISOLATED 35 | ), 36 | vpc=self.vpc, 37 | removal_policy=RemovalPolicy.DESTROY, 38 | enable_data_api=True 39 | ) 40 | 41 | # Agregar el grupo de seguridad al cluster de base de datos 42 | self.cluster.connections.allow_default_port_from(self.security_group) 43 | -------------------------------------------------------------------------------- /01-create-aurora-pgvector/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest==6.2.5 2 | -------------------------------------------------------------------------------- /01-create-aurora-pgvector/requirements.txt: -------------------------------------------------------------------------------- 1 | aws-cdk-lib==2.142.1 2 | constructs>=10.0.0,<11.0.0 3 | -------------------------------------------------------------------------------- /01-create-aurora-pgvector/source.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | rem The sole purpose of this script is to make the command 4 | rem 5 | rem source .venv/bin/activate 6 | rem 7 | rem (which activates a Python virtualenv on Linux or Mac OS X) work on Windows. 8 | rem On Windows, this command just runs this batch file (the argument is ignored). 9 | rem 10 | rem Now we don't need to document a Windows command for activating a virtualenv. 11 | 12 | echo Executing .venv\Scripts\activate.bat for you 13 | .venv\Scripts\activate.bat 14 | -------------------------------------------------------------------------------- /01-create-aurora-pgvector/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/01-create-aurora-pgvector/tests/__init__.py -------------------------------------------------------------------------------- /01-create-aurora-pgvector/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/01-create-aurora-pgvector/tests/unit/__init__.py -------------------------------------------------------------------------------- /01-create-aurora-pgvector/tests/unit/test_create_aurora_pgvector_stack.py: -------------------------------------------------------------------------------- 1 | import aws_cdk as core 2 | import aws_cdk.assertions as assertions 3 | 4 | from create_aurora_pgvector.create_aurora_pgvector_stack import CreateAuroraPgvectorStack 5 | 6 | # example tests. To run these tests, uncomment this file along with the example 7 | # resource in create_aurora_pgvector/create_aurora_pgvector_stack.py 8 | def test_sqs_queue_created(): 9 | app = core.App() 10 | stack = CreateAuroraPgvectorStack(app, "create-aurora-pgvector") 11 | template = assertions.Template.from_stack(stack) 12 | 13 | # template.has_resource_properties("AWS::SQS::Queue", { 14 | # "VisibilityTimeout": 300 15 | # }) 16 | -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | package-lock.json 3 | __pycache__ 4 | .pytest_cache 5 | .venv 6 | *.egg-info 7 | 8 | # CDK asset staging directory 9 | .cdk.staging 10 | cdk.out 11 | -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/README.md: -------------------------------------------------------------------------------- 1 | # Part 2: Create a Knowledge Base for Amazon Bedrock using Aurora PostgreSQL. 2 | 3 | Welcome to the second part of our series on building a WhatsApp-powered RAG Travel Support Agent. In this installment, we'll focus on setting up a Knowledge Base for Amazon Bedrock, leveraging the Aurora PostgreSQL database we created in [Part 1.](https://github.com/build-on-aws/rag-postgresql-agent-bedrock/tree/main/01-create-aurora-pgvector#readme) 4 | 5 | 6 | ## Overview 7 | 8 | In this part, we'll accomplish the following: 9 | 10 | 1. Set up a Knowledge Base for Amazon Bedrock 11 | 2. Configure an Amazon S3 bucket as a data source 12 | 3. Create necessary [AWS Identity and Access Management (IAM) role](https://docs.aws.amazon.com/bedrock/latest/userguide/knowledge-base-create.html#kb-create-security) and permissions 13 | 4. Store relevant information in the AWS Systems Manager Parameter Store 14 | 15 | ## The Knowledge Base Process 16 | 17 | Our Knowledge Base will automatically process unstructured text data stored in an Amazon S3 bucket. It will: 18 | 19 | 1. Convert the data into text chunks 20 | 2. Generate vector embeddings for these chunks 21 | 3. Store both the chunks and vectors in our PostgreSQL database 22 | 23 | The data we're using consists of [PDFs containing information about common](https://github.com/build-on-aws/rag-postgresql-agent-bedrock/tree/main/02-create-bedrock-knowledge-bases/airline-qa-base/PDF) issues faced by a generic airline, providing a robust foundation for our travel support agent. 24 | 25 | ## Key Components 26 | 27 | ### 1. Amazon S3 Data Source 28 | 29 | We use an S3 bucket to store our raw data (PDFs). This allows for easy updates and management of our knowledge base content. 30 | 31 | ### 2. Amazon Bedrock Knowledge Base 32 | 33 | The Knowledge Base service will handle the ingestion, processing, and storage of our data, making it readily available for our AI model to query. 34 | 35 | ### 3. IAM Role 36 | 37 | We'll create an IAM role with specific permissions to allow the Knowledge Base service to interact with other AWS services securely. 38 | 39 | ## IAM Role Permissions 40 | 41 | The [IAM role](https://github.com/build-on-aws/rag-postgresql-agent-bedrock/blob/main/02-create-bedrock-knowledge-bases/kb_role/create_role.py) we create will have the following allowed actions: 42 | 43 | - `bedrock:InvokeModel` 44 | - `secretsmanager:GetSecretValue` 45 | - `rds:DescribeDBClusters` 46 | - `rds-data:BatchExecuteStatement` 47 | - `rds-data:ExecuteStatement` 48 | - `s3:Get*` 49 | - `s3:ListBucket` 50 | 51 | These permissions enable the Knowledge Base to access the necessary AWS services and perform required operations. 52 | 53 | 54 | ## The LLM 55 | 56 | You use a LLM to convert pieces of text to a vector. It is configured to use 57 | 58 | 59 | in [create_bedrock_knowledge_bases_stack.py](./create_bedrock_knowledge_bases/create_bedrock_knowledge_bases_stack.py): 60 | ```python 61 | model_id = "amazon.titan-embed-text-v2:0" 62 | ``` 63 | 64 | Feel free to explore other supported Enbedding models, while maintaining 1024 as vector dimension. 65 | 66 | Remember to [enable model Access](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html) for this model. 67 | 68 | 69 | ## Prerequisites 70 | 71 | Before we begin, ensure you have: 72 | 73 | - Completed Parts 1 of this series 74 | 75 | 76 | ## ✅ Getting Started 77 | 78 | 1. Navigate to the project directory: 79 | ``` 80 | cd 02-create-bedrock-knowledge-bases 81 | ``` 82 | 83 | 2. Set up a virtual environment: 84 | ``` 85 | python3 -m venv .venv 86 | source .venv/bin/activate 87 | ``` 88 | For Windows: 89 | ``` 90 | .venv\Scripts\activate.bat 91 | ``` 92 | 93 | 3. Install the required dependencies: 94 | ``` 95 | pip install -r requirements.txt 96 | ``` 97 | 98 | 4. Deploy the CDK stack: 99 | ``` 100 | cdk deploy 101 | ``` 102 | 103 | ## ✅ Key Configurations 104 | 105 | - Knowledge Base configuration can be found in the [knowledgebase.py](https://github.com/build-on-aws/rag-postgresql-agent-bedrock/blob/main/02-create-bedrock-knowledge-bases/knowledge_base/knowledgebase.py) code in the CDK stack. 106 | - S3 bucket data source configuration is in the [datasource.py](https://github.com/build-on-aws/rag-postgresql-agent-bedrock/blob/main/02-create-bedrock-knowledge-bases/knowledge_base/datasource.py) code. 107 | 108 | 109 | ### Test your Knowledge Base 110 | 111 | Go to [Amazon Bedrock console](https://us-east-1.console.aws.amazon.com/bedrock/home?region=us-east-1#/knowledge-bases/) and look at your newly created Knowledge Base. In the datasources section click the Datasource **Bedrock-airline-qa**, view the Sync history, it should look like this: 112 | 113 | ![alt text](data_source.png) 114 | 115 | Use the **Test Knowledge base** tool to check if your KB is working OK. 116 | 117 | 118 | ![alt text](test_kb.png) 119 | 120 | 121 | ## 💰 Associated Costs 122 | 123 | Be mindful of the costs associated with: 124 | 125 | - [Amazon Bedrock Pricing](https://aws.amazon.com/bedrock/pricing/) 126 | - [Amazon S3 Pricing](https://aws.amazon.com/s3/pricing/) 127 | - [AWS Systems Manager pricing](https://aws.amazon.com/systems-manager/pricing/) 128 | 129 | ## Next Steps 130 | 131 | With your Knowledge Base now set up and populated with airline-related information, you're ready to move on to the next part of our series. In Part 3, we'll build an Agent for Amazon Bedrock that can search this Knowledge Base and manage data in Amazon DynamoDB, bringing us closer to our complete travel support solution. 132 | -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/airline-qa-base/PDF/Change_of_name_of_any_passenger_Lainventada_Airlines.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/02-create-bedrock-knowledge-bases/airline-qa-base/PDF/Change_of_name_of_any_passenger_Lainventada_Airlines.pdf -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/airline-qa-base/PDF/Change_of_passage_from_round_from_flown_not_flown_lastainventada_airlines.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/02-create-bedrock-knowledge-bases/airline-qa-base/PDF/Change_of_passage_from_round_from_flown_not_flown_lastainventada_airlines.pdf -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/airline-qa-base/PDF/How to know_if_can_change_the_passage_Lainventada_Airlines.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/02-create-bedrock-knowledge-bases/airline-qa-base/PDF/How to know_if_can_change_the_passage_Lainventada_Airlines.pdf -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/airline-qa-base/PDF/Knows_the_the_right_to_retract_lainventada_airlines.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/02-create-bedrock-knowledge-bases/airline-qa-base/PDF/Knows_the_the_right_to_retract_lainventada_airlines.pdf -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/airline-qa-base/PDF/Lainventada_airlines_ticket_cancellation_information.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/02-create-bedrock-knowledge-bases/airline-qa-base/PDF/Lainventada_airlines_ticket_cancellation_information.pdf -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/airline-qa-base/PDF/Request_the_refund_of_shipping_taxes_Lainventada_Airlines.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/02-create-bedrock-knowledge-bases/airline-qa-base/PDF/Request_the_refund_of_shipping_taxes_Lainventada_Airlines.pdf -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/airline-qa-base/PDF/buy_an_ticket_and_me_regretti_of_travel_Lainventada_Airlines.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/02-create-bedrock-knowledge-bases/airline-qa-base/PDF/buy_an_ticket_and_me_regretti_of_travel_Lainventada_Airlines.pdf -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/airline-qa-base/PDF/change passport_number_or_id_in_passage_lainventada_airlines.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/02-create-bedrock-knowledge-bases/airline-qa-base/PDF/change passport_number_or_id_in_passage_lainventada_airlines.pdf -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/airline-qa-base/PDF/change_of_flights_or_dates_of_ticket_lainventada_airlines.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/02-create-bedrock-knowledge-bases/airline-qa-base/PDF/change_of_flights_or_dates_of_ticket_lainventada_airlines.pdf -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/airline-qa-base/PDF/change_of_passages_after_start_the_trip_the_travel_inventada_airlines.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/02-create-bedrock-knowledge-bases/airline-qa-base/PDF/change_of_passages_after_start_the_trip_the_travel_inventada_airlines.pdf -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/airline-qa-base/PDF/change_of_passages_before_an_affectation_LAINVENTADA_Airlines.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/02-create-bedrock-knowledge-bases/airline-qa-base/PDF/change_of_passages_before_an_affectation_LAINVENTADA_Airlines.pdf -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/airline-qa-base/PDF/change_of_passages_purchased_at_agencias_lainventada_airlines..pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/02-create-bedrock-knowledge-bases/airline-qa-base/PDF/change_of_passages_purchased_at_agencias_lainventada_airlines..pdf -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/airline-qa-base/PDF/deadline for_returns_of_passages_Lainventada_Airlines.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/02-create-bedrock-knowledge-bases/airline-qa-base/PDF/deadline for_returns_of_passages_Lainventada_Airlines.pdf -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/airline-qa-base/PDF/deadlines for_make_an_change_of_passage_Lainventada_Airlines.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/02-create-bedrock-knowledge-bases/airline-qa-base/PDF/deadlines for_make_an_change_of_passage_Lainventada_Airlines.pdf -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/airline-qa-base/PDF/discover_how_how_to receive_your_devolucin_lainventada_airlines.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/02-create-bedrock-knowledge-bases/airline-qa-base/PDF/discover_how_how_to receive_your_devolucin_lainventada_airlines.pdf -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/airline-qa-base/PDF/forward_or_postponingacin_flight_the same_from_Lainventada_airline.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/02-create-bedrock-knowledge-bases/airline-qa-base/PDF/forward_or_postponingacin_flight_the same_from_Lainventada_airline.pdf -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/airline-qa-base/PDF/get_what_do_in_case_of_fraud_Lainventada_Airlines.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/02-create-bedrock-knowledge-bases/airline-qa-base/PDF/get_what_do_in_case_of_fraud_Lainventada_Airlines.pdf -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/airline-qa-base/PDF/how_to_request_the_devolucin_from_your_ticket_Lainventada_Airlines.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/02-create-bedrock-knowledge-bases/airline-qa-base/PDF/how_to_request_the_devolucin_from_your_ticket_Lainventada_Airlines.pdf -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/airline-qa-base/PDF/refunds_of_money_for_passage_launventada_airlines.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/02-create-bedrock-knowledge-bases/airline-qa-base/PDF/refunds_of_money_for_passage_launventada_airlines.pdf -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/airline-qa-base/PDF/return_for_reduction_of_shipment_taxes_lastainventada_airlines.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/02-create-bedrock-knowledge-bases/airline-qa-base/PDF/return_for_reduction_of_shipment_taxes_lastainventada_airlines.pdf -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/airline-qa-base/PDF/where to check_cost_of_change_of_passages_lainventada_Airlines.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/02-create-bedrock-knowledge-bases/airline-qa-base/PDF/where to check_cost_of_change_of_passages_lainventada_Airlines.pdf -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/airline-qa-base/PDF/where_can_change_of_passages_Lainventada_Airlines.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/02-create-bedrock-knowledge-bases/airline-qa-base/PDF/where_can_change_of_passages_Lainventada_Airlines.pdf -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/airline-qa-base/PDF/where_to_request_the_return_of_the Lainventada_Airlines ticket.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/02-create-bedrock-knowledge-bases/airline-qa-base/PDF/where_to_request_the_return_of_the Lainventada_Airlines ticket.pdf -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | 4 | import aws_cdk as cdk 5 | 6 | from create_bedrock_knowledge_bases.create_bedrock_knowledge_bases_stack import CreateBedrockKnowledgeBasesStack 7 | 8 | 9 | app = cdk.App() 10 | CreateBedrockKnowledgeBasesStack(app, "Knowledge-Base", 11 | # If you don't specify 'env', this stack will be environment-agnostic. 12 | # Account/Region-dependent features and context lookups will not work, 13 | # but a single synthesized template can be deployed anywhere. 14 | 15 | # Uncomment the next line to specialize this stack for the AWS Account 16 | # and Region that are implied by the current CLI configuration. 17 | 18 | #env=cdk.Environment(account=os.getenv('CDK_DEFAULT_ACCOUNT'), region=os.getenv('CDK_DEFAULT_REGION')), 19 | 20 | # Uncomment the next line if you know exactly what Account and Region you 21 | # want to deploy the stack to. */ 22 | 23 | #env=cdk.Environment(account='123456789012', region='us-east-1'), 24 | 25 | # For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html 26 | ) 27 | 28 | app.synth() 29 | -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "python3 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 | "**/__pycache__", 14 | "tests" 15 | ] 16 | }, 17 | "context": { 18 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 19 | "@aws-cdk/core:checkSecretUsage": true, 20 | "@aws-cdk/core:target-partitions": [ 21 | "aws", 22 | "aws-cn" 23 | ], 24 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 25 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 26 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 27 | "@aws-cdk/aws-iam:minimizePolicies": true, 28 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 29 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 30 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 31 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 32 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 33 | "@aws-cdk/core:enablePartitionLiterals": true, 34 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 35 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 36 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 37 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 38 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 39 | "@aws-cdk/aws-route53-patters:useCertificate": true, 40 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 41 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 42 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 43 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 44 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 45 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 46 | "@aws-cdk/aws-redshift:columnId": true, 47 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 48 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 49 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 50 | "@aws-cdk/aws-kms:aliasNameRef": true, 51 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 52 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 53 | "@aws-cdk/aws-efs:denyAnonymousAccess": true, 54 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, 55 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, 56 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, 57 | "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, 58 | "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, 59 | "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, 60 | "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, 61 | "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, 62 | "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, 63 | "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, 64 | "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, 65 | "@aws-cdk/aws-eks:nodegroupNameAttribute": true, 66 | "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/create_bedrock_knowledge_bases/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/02-create-bedrock-knowledge-bases/create_bedrock_knowledge_bases/__init__.py -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/create_bedrock_knowledge_bases/create_bedrock_knowledge_bases_stack.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | # Duration, 3 | Stack, 4 | aws_iam as iam, 5 | aws_bedrock as bedrock, 6 | custom_resources as cr, 7 | aws_ssm as ssm, 8 | ) 9 | from constructs import Construct 10 | from knowledge_base import KnowledgeBases, KnowledgeBaseDatasource 11 | from s3_cloudfront import S3Deploy 12 | from kb_role import CreateKBRole 13 | 14 | from get_param import get_string_param 15 | from s3_cloudfront import S3Deploy 16 | 17 | 18 | model_id = "amazon.titan-embed-text-v2:0" 19 | credentialsSecretArn = get_string_param("/pgvector/secret_arn") 20 | resourceArn_aurora = get_string_param("/pgvector/cluster_arn") 21 | ssm_table_name = get_string_param("/pgvector/table_name") 22 | tableName = f"bedrock_integration.{ssm_table_name}" 23 | 24 | common_kb_property = dict( 25 | credentialsSecretArn = credentialsSecretArn, 26 | databaseName = "kbdata", 27 | metadataField = "metadata", 28 | primaryKeyField = "id", 29 | textField = "chunks", 30 | vectorField = "embedding", 31 | resourceArn = resourceArn_aurora, 32 | tableName = tableName 33 | ) 34 | 35 | class CreateBedrockKnowledgeBasesStack(Stack): 36 | 37 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 38 | super().__init__(scope, construct_id, **kwargs) 39 | 40 | stk = Stack.of(self) 41 | _region = stk.region 42 | 43 | s3_deploy = S3Deploy(self, "S3Deploy", "airline-qa-base", "airline-qa-base") 44 | 45 | bucket_name = s3_deploy.bucket.bucket_name 46 | bucketArn = s3_deploy.bucket.bucket_arn 47 | 48 | kb_service_role = CreateKBRole( self, "CreateKBRole", credentialsSecretArn, bucket_name) 49 | 50 | base_kb_property = dict( 51 | embeddingModelArn = f"arn:aws:bedrock:{_region}::foundation-model/{model_id}", 52 | roleArn = kb_service_role.arn, **common_kb_property) 53 | description = "documents regarding to help the passenger" 54 | # Bedrock Knowledge Base 55 | bedrock_kb_property = dict(description = description, name= "la-inventada-airlines-knowledge-base", **base_kb_property) 56 | bedrock_kb = KnowledgeBases(self, "KB1", bedrock_kb_property) 57 | bedrock_kb.node.add_dependency(kb_service_role) 58 | bedrock_ds = KnowledgeBaseDatasource(self, "DS1", bedrock_kb.kb_id, "Bedrock-airlines-qa", bucketArn, "airline-qa-base",description) 59 | 60 | bedrock_ds.node.add_dependency(s3_deploy.s3deploy) 61 | 62 | ssm.StringParameter( self, "kb_ssm", parameter_name=f"/pgvector/kb_id", string_value=bedrock_kb.kb_id) 63 | -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/data_source.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/02-create-bedrock-knowledge-bases/data_source.png -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/get_param.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | ssm = boto3.client('ssm') 3 | 4 | def get_string_param(parameter_name): 5 | response = ssm.get_parameter(Name=parameter_name) 6 | parameter = response.get('Parameter') 7 | if parameter: 8 | return parameter.get('Value') 9 | else: 10 | return None -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/kb_role/__init__.py: -------------------------------------------------------------------------------- 1 | from kb_role.create_role import CreateKBRole -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/kb_role/create_role.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | # Duration, 3 | Stack, 4 | aws_iam as iam, 5 | ) 6 | from constructs import Construct 7 | 8 | class CreateKBRole(Construct): 9 | 10 | def __init__(self, scope: Construct, construct_id: str,credentialsSecretArn,bucket_name, **kwargs) -> None: 11 | super().__init__(scope, construct_id, **kwargs) 12 | 13 | 14 | stk = Stack.of(self) 15 | 16 | 17 | self.kb_service_role = iam.Role( 18 | self, 19 | "KbServiceRole", 20 | assumed_by=iam.ServicePrincipal("bedrock.amazonaws.com", 21 | conditions={ 22 | "StringEquals": { 23 | "aws:SourceAccount": stk.account 24 | }, 25 | "ArnLike": { 26 | "aws:SourceArn": f"arn:aws:bedrock:{stk.region}:{stk.account}:knowledge-base/*" 27 | } 28 | }) 29 | ) 30 | 31 | self.kb_service_role.add_to_policy(iam.PolicyStatement( 32 | effect=iam.Effect.ALLOW, 33 | actions=[ 34 | "bedrock:InvokeModel", 35 | ], 36 | resources= [ 37 | f"arn:aws:bedrock:{stk.region}::foundation-model/*" 38 | ] 39 | 40 | ), 41 | ) 42 | self.kb_service_role.add_to_policy(iam.PolicyStatement( 43 | effect=iam.Effect.ALLOW, 44 | actions=[ 45 | "secretsmanager:GetSecretValue", 46 | ], 47 | resources= [ 48 | credentialsSecretArn 49 | ] 50 | 51 | ), 52 | ) 53 | self.kb_service_role.add_to_policy(iam.PolicyStatement( 54 | effect=iam.Effect.ALLOW, 55 | actions=[ 56 | "rds:DescribeDBClusters", 57 | "rds-data:BatchExecuteStatement", 58 | "rds-data:ExecuteStatement" 59 | ], 60 | resources= [f"arn:aws:rds:{stk.region}:{stk.account}:cluster:*"] 61 | 62 | ), 63 | ) 64 | #https://docs.aws.amazon.com/cdk/api/v2/python/aws_cdk.aws_iam/ManagedPolicyProps.html 65 | 66 | ListBucket_policy = iam.PolicyStatement( 67 | effect=iam.Effect.ALLOW, 68 | actions=[ 69 | "s3:ListBucket", 70 | ], 71 | resources=[ 72 | f"arn:aws:s3:::{bucket_name}", 73 | ], 74 | conditions={ 75 | "StringEquals": { 76 | "aws:ResourceAccount": [stk.account] 77 | } 78 | }) 79 | 80 | self.kb_service_role.add_to_policy(ListBucket_policy) 81 | 82 | GetObjectStatement_policy = iam.PolicyStatement( 83 | effect=iam.Effect.ALLOW, 84 | actions=[ 85 | "s3:Get*", 86 | ], 87 | resources=[ 88 | f"arn:aws:s3:::{bucket_name}/*", 89 | ], 90 | conditions={ 91 | "StringEquals": { 92 | "aws:ResourceAccount": [ stk.account] 93 | } 94 | }) 95 | 96 | self.kb_service_role.add_to_policy(GetObjectStatement_policy) 97 | 98 | 99 | 100 | self.kb_service_role.add_managed_policy(iam.ManagedPolicy.from_aws_managed_policy_name("AmazonRDSFullAccess")) 101 | 102 | self.arn = self.kb_service_role.role_arn 103 | -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/knowledge_base/__init__.py: -------------------------------------------------------------------------------- 1 | from knowledge_base.knowledgebase import KnowledgeBases 2 | from knowledge_base.datasource import KnowledgeBaseDatasource -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/knowledge_base/datasource.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import aws_bedrock as bedrock, custom_resources as cr 2 | 3 | from constructs import Construct 4 | 5 | 6 | 7 | # https://docs.aws.amazon.com/cdk/api/v2/python/aws_cdk.aws_bedrock/CfnDataSource.html 8 | 9 | class KnowledgeBaseDatasource(Construct): 10 | def __init__(self, scope: Construct, construct_id: str, kb_id, ds_name, bucketArn, bucket_key,description, **kwargs) -> None: 11 | super().__init__(scope, construct_id, **kwargs) 12 | 13 | self.datasource = bedrock.CfnDataSource( 14 | self, 15 | "KBSampleDataSource", 16 | data_source_configuration=bedrock.CfnDataSource.DataSourceConfigurationProperty( 17 | s3_configuration=bedrock.CfnDataSource.S3DataSourceConfigurationProperty( 18 | bucket_arn=bucketArn, inclusion_prefixes=[f"{bucket_key}/"] 19 | ), 20 | type="S3", 21 | ), 22 | knowledge_base_id=kb_id, 23 | name=ds_name, 24 | data_deletion_policy="DELETE", 25 | description=description, 26 | vector_ingestion_configuration=bedrock.CfnDataSource.VectorIngestionConfigurationProperty( 27 | chunking_configuration=bedrock.CfnDataSource.ChunkingConfigurationProperty( 28 | chunking_strategy="FIXED_SIZE", 29 | fixed_size_chunking_configuration=bedrock.CfnDataSource.FixedSizeChunkingConfigurationProperty( 30 | max_tokens=300, overlap_percentage=20 31 | ), 32 | ) 33 | ), 34 | ) 35 | 36 | my_cr = cr.AwsCustomResource( 37 | self, 38 | "SyncDatasource", 39 | on_update=cr.AwsSdkCall( # will also be called for a CREATE event 40 | service="@aws-sdk/client-bedrock-agent", 41 | action="StartIngestionJob", 42 | parameters={"dataSourceId": self.datasource.attr_data_source_id, "knowledgeBaseId": kb_id}, 43 | physical_resource_id=cr.PhysicalResourceId.from_response("ingestionJob.ingestionJobId"), 44 | ), 45 | policy=cr.AwsCustomResourcePolicy.from_sdk_calls(resources=cr.AwsCustomResourcePolicy.ANY_RESOURCE), 46 | ) 47 | 48 | my_cr.node.add_dependency(self.datasource) -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/knowledge_base/knowledgebase.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | aws_bedrock as bedrock, 3 | RemovalPolicy 4 | ) 5 | from constructs import Construct 6 | 7 | 8 | class KnowledgeBases(Construct): 9 | def __init__(self, scope: Construct, construct_id: str, kb_property, **kwargs) -> None: 10 | super().__init__(scope, construct_id, **kwargs) 11 | 12 | # Create a knowledge base 13 | #https://docs.aws.amazon.com/cdk/api/v2/python/aws_cdk.aws_bedrock/CfnKnowledgeBase.html#storageconfigurationproperty 14 | self.KnowledgeBases = bedrock.CfnKnowledgeBase(self, "MyCfnKnowledgeBase", 15 | knowledge_base_configuration=bedrock.CfnKnowledgeBase.KnowledgeBaseConfigurationProperty( 16 | type="VECTOR", 17 | vector_knowledge_base_configuration=bedrock.CfnKnowledgeBase.VectorKnowledgeBaseConfigurationProperty( 18 | embedding_model_arn=kb_property["embeddingModelArn"] 19 | ) 20 | ), 21 | name=kb_property["name"], 22 | role_arn=kb_property["roleArn"], 23 | storage_configuration=bedrock.CfnKnowledgeBase.StorageConfigurationProperty( 24 | #type for rds aurora 25 | type="RDS", 26 | rds_configuration=bedrock.CfnKnowledgeBase.RdsConfigurationProperty( 27 | credentials_secret_arn=kb_property["credentialsSecretArn"], 28 | database_name=kb_property["databaseName"], 29 | field_mapping=bedrock.CfnKnowledgeBase.RdsFieldMappingProperty( 30 | metadata_field=kb_property["metadataField"], 31 | primary_key_field=kb_property["primaryKeyField"], 32 | text_field=kb_property["textField"], 33 | vector_field=kb_property["vectorField"] 34 | ), 35 | resource_arn=kb_property["resourceArn"], 36 | table_name=kb_property["tableName"] 37 | ) 38 | ), 39 | 40 | # the properties below are optional 41 | description=kb_property["description"], 42 | ) 43 | self.KnowledgeBases.apply_removal_policy(RemovalPolicy.DESTROY) 44 | 45 | self.kb_id = self.KnowledgeBases.attr_knowledge_base_id 46 | -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest==6.2.5 2 | -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/requirements.txt: -------------------------------------------------------------------------------- 1 | aws-cdk-lib==2.142.1 2 | constructs>=10.0.0,<11.0.0 3 | boto3 4 | -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/s3_cloudfront/__init__.py: -------------------------------------------------------------------------------- 1 | from s3_cloudfront.s3_cloudfront_website import S3Deploy 2 | -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/s3_cloudfront/s3_cloudfront_website.py: -------------------------------------------------------------------------------- 1 | from constructs import Construct 2 | from aws_cdk import ( 3 | aws_s3_deployment as s3deploy, 4 | aws_s3 as s3, RemovalPolicy) 5 | 6 | 7 | class S3Deploy(Construct): 8 | def __init__(self, scope: Construct, id: str, files_location, dest_prefix,**kwargs) -> None: 9 | super().__init__(scope, id, **kwargs) 10 | 11 | self.bucket = s3.Bucket(self, "Bucket",access_control=s3.BucketAccessControl.PRIVATE, removal_policy=RemovalPolicy.DESTROY) 12 | 13 | 14 | self.s3deploy = s3deploy.BucketDeployment(self, "Deployment", 15 | sources=[s3deploy.Source.asset(files_location)], 16 | destination_bucket = self.bucket, 17 | retain_on_delete=False, 18 | destination_key_prefix=dest_prefix 19 | ) 20 | 21 | 22 | def deploy(self, id, files_loc, prefix): 23 | deployment = s3deploy.BucketDeployment(self, id, 24 | sources=[s3deploy.Source.asset(files_loc)], 25 | destination_bucket = self.bucket, 26 | retain_on_delete=False, 27 | destination_key_prefix=prefix 28 | ) 29 | return deployment 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/source.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | rem The sole purpose of this script is to make the command 4 | rem 5 | rem source .venv/bin/activate 6 | rem 7 | rem (which activates a Python virtualenv on Linux or Mac OS X) work on Windows. 8 | rem On Windows, this command just runs this batch file (the argument is ignored). 9 | rem 10 | rem Now we don't need to document a Windows command for activating a virtualenv. 11 | 12 | echo Executing .venv\Scripts\activate.bat for you 13 | .venv\Scripts\activate.bat 14 | -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/test_kb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/02-create-bedrock-knowledge-bases/test_kb.png -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/02-create-bedrock-knowledge-bases/tests/__init__.py -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/02-create-bedrock-knowledge-bases/tests/unit/__init__.py -------------------------------------------------------------------------------- /02-create-bedrock-knowledge-bases/tests/unit/test_create_bedrock_knowledge_bases_stack.py: -------------------------------------------------------------------------------- 1 | import aws_cdk as core 2 | import aws_cdk.assertions as assertions 3 | 4 | from create_bedrock_knowledge_bases.create_bedrock_knowledge_bases_stack import CreateBedrockKnowledgeBasesStack 5 | 6 | # example tests. To run these tests, uncomment this file along with the example 7 | # resource in create_bedrock_knowledge_bases/create_bedrock_knowledge_bases_stack.py 8 | def test_sqs_queue_created(): 9 | app = core.App() 10 | stack = CreateBedrockKnowledgeBasesStack(app, "create-bedrock-knowledge-bases") 11 | template = assertions.Template.from_stack(stack) 12 | 13 | # template.has_resource_properties("AWS::SQS::Queue", { 14 | # "VisibilityTimeout": 300 15 | # }) 16 | -------------------------------------------------------------------------------- /03-rag-agent-bedrock/.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | package-lock.json 3 | __pycache__ 4 | .pytest_cache 5 | .venv 6 | *.egg-info 7 | 8 | # CDK asset staging directory 9 | .cdk.staging 10 | cdk.out 11 | -------------------------------------------------------------------------------- /03-rag-agent-bedrock/03_agent.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/03-rag-agent-bedrock/03_agent.jpg -------------------------------------------------------------------------------- /03-rag-agent-bedrock/README.md: -------------------------------------------------------------------------------- 1 | # Part 3: Create an Agent for Amazon Bedrock that can search a knowledge base, as well as insert and query data in an Amazon DynamoDB database. 2 | 3 | Welcome to the third installment of our series on building a WhatsApp-powered RAG Travel Support Agent. In our previous parts, we laid the groundwork by setting up an Amazon Aurora PostgreSQL vector database and creating a Knowledge Base for Amazon Bedrock. Now, we're ready to take a significant step forward by creating an intelligent Agent for Amazon Bedrock that can efficiently search our Knowledge Base and manage data within an Amazon DynamoDB table. 4 | 5 | ## Overview of the Setup Process 6 | 7 | In this part, with [AWS Cloud Development Kit (CDK)](https://aws.amazon.com/cdk) for Pytho we'll create an Agent for Amazon Bedrock that serves as the interface between the user and our AI-powered travel support system. This agent will be capable of: 8 | 9 | 1. Searching the Knowledge Base we created in Part 2 for relevant travel information. 10 | 2. Interacting with an Amazon DynamoDB table to manage passenger ([with this data](https://github.com/build-on-aws/rag-postgresql-agent-bedrock/blob/main/03-rag-agent-bedrock/lambdas/code/dynamodb_put_item_from_csv/dataset.csv)) [from Kaggle](https://www.kaggle.com/datasets/iamsouravbanerjee/airline-dataset). information and support tickets. 11 | 3. Performing various actions through predefined action groups, including retrieving passenger data, creating support tickets, and checking ticket status. 12 | 13 | ## Key Components 14 | 15 | This agent will include the following key components: 16 | 17 | - Knowledge Base integration for contextual information retrieval. 18 | - Action groups for specific tasks (AskTodayDAy, GetPassengerInformation, CreateSupportTicket, GetSupporTicketStatus) 19 | - AWS Lambda functions to handle backend logic for each action group. 20 | - Amazon DynamoDB table interactions for data management. 21 | - Automated date and time retrieval functionality. 22 | 23 | ![Digrama parte 1](03_agent.jpg) 24 | 25 | ## Prerequisites 26 | 27 | - Completed Parts 1 and 2 of this series 28 | 29 | ## ✅ Key Configurations 30 | 31 | - Agent for Amazon Bedrock configuration can be found in the [create_agent_with_kb_ag.py](https://github.com/build-on-aws/rag-postgresql-agent-bedrock/blob/main/03-rag-agent-bedrock/agent_bedrock/create_agent_with_kb_ag.py) code in the CDK stack. 32 | - [AWS Identity and Access Management (IAM) role](https://docs.aws.amazon.com/bedrock/latest/userguide/knowledge-base-create.html#kb-create-security) for Agent permision is in the [create_role.py](https://github.com/build-on-aws/rag-postgresql-agent-bedrock/blob/main/03-rag-agent-bedrock/agent_role/create_role.py) code. 33 | - Principal Stack. 34 | 35 | ### LLM Use 36 | 37 | In this project you use Anthropic's Claude 3 Sonnet. A good balance in speed and capabilities. If you want to change it to [other supported model](https://docs.aws.amazon.com/bedrock/latest/userguide/agents-supported.html) go to [agent_data.json](./rag_agent_bedrock/agent_data.json) 38 | 39 | 40 | ```json 41 | "foundation_model": "anthropic.claude-3-sonnet-20240229-v1:0" 42 | ``` 43 | 44 | 45 | ## ✅ Getting Started 46 | 47 | Let's set up our development environment: 48 | 49 | 1. Navigate to the project directory: 50 | ``` 51 | cd 03-rag-agent-bedrock 52 | ``` 53 | 54 | 2. Create and activate a virtual environment: 55 | ``` 56 | python3 -m venv .venv 57 | source .venv/bin/activate 58 | ``` 59 | For Windows: 60 | ``` 61 | .venv\Scripts\activate.bat 62 | ``` 63 | 64 | 3. Install the required dependencies: 65 | ``` 66 | pip install -r requirements.txt 67 | ``` 68 | 69 | 4. Deploy the CDK stack: 70 | ``` 71 | cdk deploy 72 | ``` 73 | 74 | ### Test the Agent 75 | 76 | Go to [Amazon Bedrock > Agents](https://us-east-1.console.aws.amazon.com/bedrock/home?region=us-east-1#/agents) and simulate the interaction: 77 | 78 | | Reschedule a Flight | Lost Luggage | 79 | |-----------------|-----------------| 80 | | ![alt text](reschedule_flight.gif) | ![alt text](ticket.gif) | 81 | 82 | You can use passenger ID : *90896710* (or other from the [synthetic data](https://github.com/build-on-aws/rag-postgresql-agent-bedrock/blob/main/03-rag-agent-bedrock/lambdas/code/dynamodb_put_item_from_csv/dataset.csv) created in the DynamoDB Table ) 83 | 84 | 85 | 86 | ## 💰 Associated Costs 87 | 88 | - [Amazon Bedrock Pricing](https://aws.amazon.com/bedrock/pricing/) 89 | - [Amazon S3 Pricing](https://aws.amazon.com/s3/pricing/) 90 | - [AWS Systems Manager pricing](https://aws.amazon.com/systems-manager/pricing/) 91 | 92 | -------------------------------------------------------------------------------- /03-rag-agent-bedrock/agent_bedrock/__init__.py: -------------------------------------------------------------------------------- 1 | from agent_bedrock.create_agent_with_kb_ag import CreateAgentWithKA -------------------------------------------------------------------------------- /03-rag-agent-bedrock/agent_bedrock/create_agent_with_kb_ag.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | # Duration, 3 | Stack, 4 | aws_dynamodb as ddb, 5 | RemovalPolicy, 6 | aws_iam as iam, 7 | aws_bedrock as bedrock 8 | 9 | # aws_sqs as sqs, 10 | ) 11 | from constructs import Construct 12 | 13 | class CreateAgentWithKA(Construct): 14 | def __init__(self, scope: Construct, construct_id: str,agent_name,foundation_model, agent_instruction, description,agent_knowledge_base_property,agent_action_group_property,agent_resource_role, **kwargs) -> None: 15 | super().__init__(scope, construct_id, **kwargs) 16 | 17 | self.cfn_agent = bedrock.CfnAgent(self, "LaInventadaAgent", 18 | agent_name=agent_name, 19 | description=description, 20 | auto_prepare = True, 21 | idle_session_ttl_in_seconds = 600, 22 | skip_resource_in_use_check_on_delete=False, 23 | test_alias_tags={ 24 | "test_alias_tags_key": "LaInventadaAgent" 25 | }, 26 | knowledge_bases = agent_knowledge_base_property, 27 | action_groups = agent_action_group_property, 28 | agent_resource_role_arn = agent_resource_role, 29 | foundation_model=foundation_model, 30 | instruction=agent_instruction, 31 | ) 32 | self.cfn_agent.apply_removal_policy(RemovalPolicy.DESTROY) 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /03-rag-agent-bedrock/agent_role/__init__.py: -------------------------------------------------------------------------------- 1 | from agent_role.create_role import CreateAgentRole -------------------------------------------------------------------------------- /03-rag-agent-bedrock/agent_role/create_role.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | # Duration, 3 | Stack, 4 | aws_iam as iam, 5 | ) 6 | from constructs import Construct 7 | 8 | class CreateAgentRole(Construct): 9 | 10 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 11 | super().__init__(scope, construct_id, **kwargs) 12 | 13 | stk = Stack.of(self) 14 | _account = stk.account 15 | _region = stk.region 16 | _stack_name = stk.stack_name 17 | 18 | self.kb_service_role = iam.Role( self, "Kb", 19 | role_name= f'AmazonBedrockExecutionRoleForAgents_{_stack_name}', 20 | assumed_by=iam.ServicePrincipal("bedrock.amazonaws.com", 21 | conditions={ 22 | "StringEquals": { "aws:SourceAccount": _account}, 23 | "ArnLike": { "aws:SourceArn": f"arn:aws:bedrock:{_region}:{_account}:agent/*"} 24 | })) 25 | 26 | self.kb_service_role.add_to_policy(iam.PolicyStatement( 27 | effect=iam.Effect.ALLOW, 28 | actions=[ "bedrock:InvokeModel"], 29 | resources=[ f"arn:aws:bedrock:{_region}::foundation-model/*"])) 30 | 31 | 32 | self.knowledge_base_policy = iam.PolicyStatement( 33 | effect=iam.Effect.ALLOW, 34 | actions=["bedrock:Retrieve"], 35 | resources=[f"arn:aws:bedrock:{_region}:{_account}:knowledge-base/*"]) 36 | 37 | self.kb_service_role.add_to_policy(self.knowledge_base_policy) 38 | self.arn = self.kb_service_role.role_arn 39 | 40 | 41 | -------------------------------------------------------------------------------- /03-rag-agent-bedrock/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | 4 | import aws_cdk as cdk 5 | 6 | from rag_agent_bedrock.rag_agent_bedrock_stack import RagAgentBedrockStack 7 | 8 | 9 | app = cdk.App() 10 | RagAgentBedrockStack(app, "RagAgentBedrockStack", 11 | # If you don't specify 'env', this stack will be environment-agnostic. 12 | # Account/Region-dependent features and context lookups will not work, 13 | # but a single synthesized template can be deployed anywhere. 14 | 15 | # Uncomment the next line to specialize this stack for the AWS Account 16 | # and Region that are implied by the current CLI configuration. 17 | 18 | #env=cdk.Environment(account=os.getenv('CDK_DEFAULT_ACCOUNT'), region=os.getenv('CDK_DEFAULT_REGION')), 19 | 20 | # Uncomment the next line if you know exactly what Account and Region you 21 | # want to deploy the stack to. */ 22 | 23 | #env=cdk.Environment(account='123456789012', region='us-east-1'), 24 | 25 | # For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html 26 | ) 27 | 28 | app.synth() 29 | -------------------------------------------------------------------------------- /03-rag-agent-bedrock/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "python3 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 | "**/__pycache__", 14 | "tests" 15 | ] 16 | }, 17 | "context": { 18 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 19 | "@aws-cdk/core:checkSecretUsage": true, 20 | "@aws-cdk/core:target-partitions": [ 21 | "aws", 22 | "aws-cn" 23 | ], 24 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 25 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 26 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 27 | "@aws-cdk/aws-iam:minimizePolicies": true, 28 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 29 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 30 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 31 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 32 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 33 | "@aws-cdk/core:enablePartitionLiterals": true, 34 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 35 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 36 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 37 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 38 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 39 | "@aws-cdk/aws-route53-patters:useCertificate": true, 40 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 41 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 42 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 43 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 44 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 45 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 46 | "@aws-cdk/aws-redshift:columnId": true, 47 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 48 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 49 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 50 | "@aws-cdk/aws-kms:aliasNameRef": true, 51 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 52 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 53 | "@aws-cdk/aws-efs:denyAnonymousAccess": true, 54 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, 55 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, 56 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, 57 | "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, 58 | "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, 59 | "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, 60 | "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, 61 | "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, 62 | "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, 63 | "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, 64 | "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, 65 | "@aws-cdk/aws-eks:nodegroupNameAttribute": true, 66 | "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /03-rag-agent-bedrock/databases/__init__.py: -------------------------------------------------------------------------------- 1 | from databases.databases import Tables -------------------------------------------------------------------------------- /03-rag-agent-bedrock/databases/databases.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | RemovalPolicy, 3 | aws_dynamodb as ddb 4 | ) 5 | from constructs import Construct 6 | 7 | 8 | REMOVAL_POLICY = RemovalPolicy.DESTROY 9 | 10 | TABLE_CONFIG = dict (removal_policy=REMOVAL_POLICY, billing_mode= ddb.BillingMode.PAY_PER_REQUEST) 11 | 12 | 13 | class Tables(Construct): 14 | 15 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 16 | super().__init__(scope, construct_id, **kwargs) 17 | 18 | self.passangerTable = ddb.Table( 19 | self, "passangerTable", 20 | partition_key=ddb.Attribute(name="passenger_id", type=ddb.AttributeType.STRING), 21 | **TABLE_CONFIG) 22 | 23 | self.ticketTable = ddb.Table( 24 | self, "ticketTable", 25 | partition_key=ddb.Attribute(name="ticket_number", type=ddb.AttributeType.STRING), 26 | **TABLE_CONFIG) 27 | 28 | -------------------------------------------------------------------------------- /03-rag-agent-bedrock/get_param.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | ssm = boto3.client('ssm') 3 | 4 | def get_string_param(parameter_name): 5 | response = ssm.get_parameter(Name=parameter_name) 6 | parameter = response.get('Parameter') 7 | if parameter: 8 | return parameter.get('Value') 9 | else: 10 | return None -------------------------------------------------------------------------------- /03-rag-agent-bedrock/lambdas/__init__.py: -------------------------------------------------------------------------------- 1 | from lambdas.project_lambdas import Lambdas 2 | from lambdas.datasource import DynamodbWithSampleDataStack 3 | -------------------------------------------------------------------------------- /03-rag-agent-bedrock/lambdas/code/ask_date/lambda_function.py: -------------------------------------------------------------------------------- 1 | ############################################################### 2 | ## This function is to populate a dynamoDB table with a CSV ### 3 | ############################################################### 4 | 5 | import datetime 6 | 7 | 8 | BASE_PATH = '/tmp/' 9 | CSV_SEPARATOR = ';' 10 | 11 | 12 | def lambda_handler(event, contex): 13 | agent = event['agent'] 14 | actionGroup = event['actionGroup'] 15 | function = event['function'] 16 | 17 | # Obtener la fecha y hora actual 18 | ahora = datetime.datetime.now() 19 | 20 | # Obtener el día de la semana 21 | dia_semana = ahora.strftime("%A") 22 | 23 | # Obtener la fecha en formato día/mes/año 24 | fecha = ahora.strftime("%d/%m/%Y") 25 | 26 | # Obtener la hora en formato hora:minutos:segundos 27 | hora = ahora.strftime("%H:%M:%S") 28 | 29 | # Imprimir el resultado 30 | print(f"Today is {dia_semana}, {fecha}") 31 | print(f"Date {hora}") 32 | 33 | today = {"day": dia_semana, "date": fecha, "time": hora} 34 | 35 | responseBody = { 36 | "TEXT": { 37 | "body": "The date and hour from today is {}".format(today) 38 | } 39 | } 40 | print("Response: {}".format(responseBody)) 41 | 42 | action_response = { 43 | 'actionGroup': actionGroup, 44 | 'function': function, 45 | 'functionResponse': { 46 | 'responseBody': responseBody 47 | } 48 | 49 | } 50 | 51 | dummy_function_response = {'response': action_response, 'messageVersion': event['messageVersion']} 52 | print("Response: {}".format(dummy_function_response)) 53 | 54 | return dummy_function_response 55 | -------------------------------------------------------------------------------- /03-rag-agent-bedrock/lambdas/code/dynamodb_put_item_from_csv/lambda_function.py: -------------------------------------------------------------------------------- 1 | ############################################################### 2 | ## This function is to populate a dynamoDB table with a CSV ### 3 | ############################################################### 4 | 5 | import json 6 | import requests 7 | import boto3 8 | import botocore.exceptions 9 | import csv 10 | import json 11 | 12 | 13 | dynamodb = boto3.resource('dynamodb') 14 | 15 | 16 | 17 | def lambda_handler(event, context): 18 | '''Handle Lambda event from AWS''' 19 | # Setup alarm for remaining runtime minus a second 20 | # signal.alarm((context.get_remaining_time_in_millis() / 1000) - 1) 21 | try: 22 | 23 | print('REQUEST RECEIVED:', event) 24 | print('CONTEXT RECEIVED:', context) 25 | 26 | if event['RequestType'] == 'Create': 27 | print('CREATE!') 28 | event['PhysicalResourceId'] = 'NOT_YET' 29 | load_data(event, context) 30 | 31 | elif event['RequestType'] == 'Update': 32 | print('UPDATE!') 33 | load_data(event, context) 34 | send_response(event, context, "SUCCESS",{"Message": "Resource update successful!"}) 35 | 36 | elif event['RequestType'] == 'Delete': 37 | print('DELETE!') 38 | send_response(event, context, "SUCCESS",{"Message": "Resource delete successful!"}) 39 | else: 40 | print('FAILED!') 41 | send_response(event, context, "FAILED", 42 | {"Message": "Unexpected event received from CloudFormation"}) 43 | except Exception as error: 44 | print('FAILED!', error) 45 | send_response(event, context, "FAILED", { 46 | "Message": "Exception during processing"}) 47 | 48 | 49 | def load_data (event, context): 50 | 51 | if "ResourceProperties" in event: 52 | print ("create_datasource") 53 | props = event['ResourceProperties'] 54 | 55 | print("props:",props) 56 | table_name = props['table_name'] 57 | sample_data_file = props['sample_data_file'] 58 | table = dynamodb.Table(table_name) 59 | 60 | rows = [] 61 | 62 | 63 | with open(sample_data_file) as csv_file: 64 | csv_reader = csv.DictReader(csv_file) 65 | for row in csv_reader: 66 | rows.append(dict(row)) 67 | 68 | 69 | with table.batch_writer() as batch: 70 | for item in rows: 71 | batch.put_item(Item=item) 72 | 73 | 74 | event['PhysicalResourceId'] = f"{table_name}|{len(rows)}" 75 | send_response(event, context, "SUCCESS",{"Message": "Resource creation successful!"}) 76 | 77 | else: 78 | print("no resource properties!") 79 | 80 | 81 | def send_response(event, context, response_status, response_data): 82 | '''Send a resource manipulation status response to CloudFormation''' 83 | response_body = json.dumps({ 84 | "Status": response_status, 85 | "Reason": "See the details in CloudWatch Log Stream: " + context.log_stream_name, 86 | "PhysicalResourceId": event['PhysicalResourceId'] if 'PhysicalResourceId' in event else "NOPHYID", 87 | "StackId": event['StackId'], 88 | "RequestId": event['RequestId'], 89 | "LogicalResourceId": event['LogicalResourceId'], 90 | "Data": response_data 91 | }) 92 | headers = { 93 | 'Content-Type': 'application/json', 94 | 'Content-Length': str(len(response_body)) 95 | } 96 | 97 | 98 | print('ResponseURL: ', event['ResponseURL']) 99 | print('ResponseBody:', response_body) 100 | 101 | response = requests.put(event['ResponseURL'], 102 | data=response_body, headers=headers) 103 | 104 | print("Status code:", response.status_code) 105 | print("Status message:", response.text) 106 | 107 | return response 108 | 109 | 110 | def timeout_handler(_signal, _frame): 111 | '''Handle SIGALRM''' 112 | raise Exception('Time exceeded') 113 | 114 | # signal.signal(signal.SIGALRM, timeout_handler) 115 | -------------------------------------------------------------------------------- /03-rag-agent-bedrock/lambdas/code/dynamodb_put_item_random_key/lambda_function.py: -------------------------------------------------------------------------------- 1 | ########################################################### 2 | ## This function is to put item with a random key value ### 3 | ########################################################### 4 | 5 | import json 6 | import csv 7 | import boto3 8 | import os 9 | import time 10 | import random 11 | 12 | BASE_PATH = '/tmp/' 13 | CSV_SEPARATOR = ';' 14 | 15 | ddb = boto3.resource('dynamodb') 16 | table = ddb.Table(os.environ['TABLE_NAME']) 17 | key_name = os.environ.get('ENV_KEY_NAME') 18 | 19 | def generate_random_4_digit_number(): 20 | return random.randint(1000, 9999) 21 | 22 | def save_item_ddb(table,item): 23 | response = table.put_item(Item=item) 24 | return response 25 | 26 | 27 | def lambda_handler(event, contex): 28 | agent = event['agent'] 29 | actionGroup = event['actionGroup'] 30 | function = event['function'] 31 | parameters = event.get('parameters', []) 32 | 33 | # Execute your business logic here. For more information, refer to: https://docs.aws.amazon.com/bedrock/latest/userguide/agents-lambda.html 34 | 35 | item_value = {} 36 | 37 | for n in parameters: 38 | print(n) 39 | item = n["name"] 40 | item_value[item] = n["value"] 41 | 42 | print(item_value) 43 | 44 | #Create a Random key value 45 | item_value[key_name] = str(generate_random_4_digit_number()) 46 | print(f"{key_name}: ",item_value) 47 | response = save_item_ddb(table,item_value) 48 | print(response) 49 | 50 | if response['ResponseMetadata']['HTTPStatusCode'] == 200: 51 | responseBody = { 52 | "TEXT": { 53 | "body": "The function {} was called successfully, value to database: ".format(item_value) 54 | } 55 | } 56 | else: 57 | responseBody = { 58 | "TEXT": { 59 | "body": "The function {} with error!".format(response) 60 | } 61 | } 62 | 63 | action_response = { 64 | 'actionGroup': actionGroup, 65 | 'function': function, 66 | 'functionResponse': { 67 | 'responseBody': responseBody 68 | } 69 | 70 | } 71 | 72 | dummy_function_response = {'response': action_response, 'messageVersion': event['messageVersion']} 73 | print("Response: {}".format(dummy_function_response)) 74 | 75 | return dummy_function_response 76 | -------------------------------------------------------------------------------- /03-rag-agent-bedrock/lambdas/code/dynamodb_query/lambda_function.py: -------------------------------------------------------------------------------- 1 | ################################################# 2 | ## This function is to query a dynamoDB table ### 3 | ################################################# 4 | 5 | import json 6 | from boto3.dynamodb.conditions import Key 7 | import boto3 8 | import botocore.exceptions 9 | import os 10 | import re 11 | 12 | dynamodb_resource=boto3.resource('dynamodb') 13 | 14 | table_name = os.environ.get('TABLE_NAME') 15 | key_name = os.environ.get('ENV_KEY_NAME') 16 | 17 | table = dynamodb_resource.Table(table_name) 18 | 19 | def db_query(key,table,keyvalue): 20 | response = table.query( 21 | KeyConditionExpression=Key(key).eq(keyvalue) 22 | ) 23 | print(response) 24 | return response 25 | 26 | def lambda_handler(event, context): 27 | print(event) 28 | agent = event['agent'] 29 | actionGroup = event['actionGroup'] 30 | function = event['function'] 31 | parameters = event.get('parameters', []) 32 | 33 | item_value = {} 34 | 35 | for n in parameters: 36 | print(n) 37 | item = n["name"] 38 | item_value[item] = n["value"] 39 | 40 | print(item_value) 41 | 42 | #item_value = json.loads(item) 43 | item_value[key_name] = item_value[key_name].replace("+","") 44 | 45 | '''Handle Lambda event from AWS''' 46 | # Setup alarm for remaining runtime minus a second 47 | # signal.alarm((context.get_remaining_time_in_millis() / 1000) - 1) 48 | try: 49 | print('REQUEST RECEIVED:', event) 50 | print('REQUEST CONTEXT:', context) 51 | 52 | print("phone_number: ",item_value[key_name]) 53 | phone_number = item_value[key_name] 54 | s = re.sub(r'[^a-zA-Z0-9]', '', phone_number) 55 | tabla_response = db_query(key_name,table,str(s)) 56 | print(tabla_response) 57 | 58 | 59 | if tabla_response['ResponseMetadata']['HTTPStatusCode'] == 200: 60 | responseBody = { 61 | "TEXT": { 62 | "body": "The query response is {}".format(tabla_response['Items'][0]) 63 | } 64 | } 65 | else: 66 | responseBody = { 67 | "TEXT": { 68 | "body": "The query with error {}".format(tabla_response['Items'][0]) 69 | } 70 | } 71 | 72 | action_response = { 73 | 'actionGroup': actionGroup, 74 | 'function': function, 75 | 'functionResponse': { 76 | 'responseBody': responseBody 77 | } 78 | 79 | } 80 | 81 | dummy_function_response = {'response': action_response, 'messageVersion': event['messageVersion']} 82 | print("Response: {}".format(dummy_function_response)) 83 | 84 | return dummy_function_response 85 | 86 | 87 | 88 | except Exception as error: 89 | print('FAILED!', error) 90 | responseBody = { 91 | "TEXT": { 92 | "body": "The query with error {}".format(error) 93 | } 94 | } 95 | 96 | action_response = { 97 | 'actionGroup': actionGroup, 98 | 'function': function, 99 | 'functionResponse': { 100 | 'responseBody': responseBody 101 | } 102 | 103 | } 104 | dummy_function_response = {'response': action_response, 'messageVersion': event['messageVersion']} 105 | print("Response: {}".format(dummy_function_response)) 106 | 107 | return dummy_function_response 108 | 109 | -------------------------------------------------------------------------------- /03-rag-agent-bedrock/lambdas/datasource.py: -------------------------------------------------------------------------------- 1 | 2 | from aws_cdk import ( 3 | # Duration, 4 | Stack, 5 | # aws_sqs as sqs, 6 | CustomResource, 7 | CfnOutput 8 | ) 9 | from constructs import Construct 10 | from databases import Tables 11 | from lambdas import Lambdas 12 | 13 | 14 | class DynamodbWithSampleDataStack(Construct): 15 | def __init__( 16 | self, scope: Construct, 17 | construct_id: str, 18 | table, 19 | lambda_function, 20 | file_name, 21 | **kwargs) -> None: 22 | 23 | super().__init__(scope, construct_id, **kwargs) 24 | 25 | 26 | table.grant_full_access(lambda_function) 27 | CfnOutput(self, "table", value=table.table_name) 28 | 29 | ds = CustomResource( 30 | self, 31 | "S3DS", 32 | resource_type="Custom::CSVDataSource", 33 | service_token=lambda_function.function_arn, 34 | properties=dict( 35 | table_name=table.table_name, sample_data_file=file_name 36 | ), 37 | ) 38 | -------------------------------------------------------------------------------- /03-rag-agent-bedrock/lambdas/project_lambdas.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from aws_cdk import aws_lambda, Duration, aws_iam as iam 4 | 5 | from constructs import Construct 6 | 7 | 8 | LAMBDA_TIMEOUT = 900 9 | 10 | BASE_LAMBDA_CONFIG = dict( 11 | timeout=Duration.seconds(LAMBDA_TIMEOUT), 12 | memory_size=128, 13 | tracing=aws_lambda.Tracing.ACTIVE, 14 | architecture=aws_lambda.Architecture.ARM_64 15 | ) 16 | 17 | PYTHON_LAMBDA_CONFIG = dict( 18 | runtime=aws_lambda.Runtime.PYTHON_3_11, **BASE_LAMBDA_CONFIG 19 | ) 20 | 21 | from layers import Layers 22 | 23 | class Lambdas(Construct): 24 | def __init__(self, scope: Construct, construct_id: str, self_account, **kwargs) -> None: 25 | super().__init__(scope, construct_id, **kwargs) 26 | 27 | COMMON_LAMBDA_CONF = dict(environment={}, **PYTHON_LAMBDA_CONFIG) 28 | 29 | Lay = Layers(self, 'Lay') 30 | 31 | self.dynamodb_put_item = aws_lambda.Function( 32 | self, "DynamoDB_put_item", 33 | description ="Put items to csv to DynamoDB" , 34 | handler="lambda_function.lambda_handler", 35 | layers= [Lay.bs4_requests, Lay.common], 36 | code=aws_lambda.Code.from_asset("./lambdas/code/dynamodb_put_item_from_csv"), 37 | **COMMON_LAMBDA_CONF) 38 | 39 | 40 | self.dynamodb_query_passanger = aws_lambda.Function( 41 | self, "query_dynamodb_passanger", 42 | description ="Query DynamoDB Passanger Table" , 43 | handler="lambda_function.lambda_handler", 44 | code=aws_lambda.Code.from_asset("./lambdas/code/dynamodb_query"), 45 | **COMMON_LAMBDA_CONF) 46 | 47 | self.dynamodb_query_ticket = aws_lambda.Function( 48 | self, "query_dynamodb_ticket", 49 | description ="Query DynamoDB Ticket Table" , 50 | handler="lambda_function.lambda_handler", 51 | code=aws_lambda.Code.from_asset("./lambdas/code/dynamodb_query"), 52 | **COMMON_LAMBDA_CONF) 53 | 54 | self.ask_date = aws_lambda.Function( 55 | self, "ask_date", 56 | description ="Ask today Date" , 57 | handler="lambda_function.lambda_handler", 58 | code=aws_lambda.Code.from_asset("./lambdas/code/ask_date"), 59 | **COMMON_LAMBDA_CONF) 60 | 61 | self.dynamodb_put_item_random_key = aws_lambda.Function( 62 | self, "dynamodb_put_item_random_key", 63 | description ="put item with a random key value" , 64 | handler="lambda_function.lambda_handler", 65 | code=aws_lambda.Code.from_asset("./lambdas/code/dynamodb_put_item_random_key"), 66 | **COMMON_LAMBDA_CONF) 67 | 68 | for f in [self.dynamodb_query_passanger,self.ask_date,self.dynamodb_query_ticket,self.dynamodb_put_item_random_key]: 69 | f.add_permission( 70 | f'invoke from account', 71 | principal=iam.ServicePrincipal("bedrock.amazonaws.com"), 72 | action="lambda:invokeFunction", 73 | # source_arn=f"arn:aws:lambda:{self.region}:{self.account}:*") 74 | ) 75 | -------------------------------------------------------------------------------- /03-rag-agent-bedrock/layers/__init__.py: -------------------------------------------------------------------------------- 1 | from layers.project_layers import Layers -------------------------------------------------------------------------------- /03-rag-agent-bedrock/layers/aiofile-amazon-transcribe.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/03-rag-agent-bedrock/layers/aiofile-amazon-transcribe.zip -------------------------------------------------------------------------------- /03-rag-agent-bedrock/layers/bs4_requests.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/03-rag-agent-bedrock/layers/bs4_requests.zip -------------------------------------------------------------------------------- /03-rag-agent-bedrock/layers/common/python/agent_utils.py: -------------------------------------------------------------------------------- 1 | 2 | from langchain import LLMMathChain 3 | from langchain.llms.bedrock import Bedrock 4 | from langchain.memory import ConversationBufferMemory 5 | from langchain.agents import load_tools, initialize_agent, AgentType,Tool 6 | 7 | 8 | def match_function(model_id,bedrock_client): 9 | math_chain_llm = Bedrock(model_id=model_id,model_kwargs={"temperature":0,"stop_sequences" : ["```output"]},client=bedrock_client) 10 | llm_math_chain = LLMMathChain(llm=math_chain_llm, verbose=True) 11 | 12 | llm_math_chain.llm_chain.prompt.template = """Human: Given a question with a math problem, provide only a single line mathematical expression that solves the problem in the following format. Don't solve the expression only create a parsable expression. 13 | ```text 14 | ${{single line mathematical expression that solves the problem}} 15 | ``` 16 | 17 | Assistant: 18 | Here is an example response with a single line mathematical expression for solving a math problem: 19 | ```text 20 | 37593**(1/5) 21 | ``` 22 | 23 | Human: {question} 24 | Assistant:""" 25 | return Tool.from_function( 26 | func=llm_math_chain.run, 27 | name="Calculator", 28 | description="Useful for when you need to answer questions about math.", 29 | ) 30 | 31 | def memory_dynamodb(id,table_name_session,llm): 32 | message_history = DynamoDBChatMessageHistory(table_name=table_name_session, session_id=id) 33 | memory = ConversationBufferMemory( 34 | memory_key="chat_history", llm=llm,max_token_limit=800,chat_memory=message_history, return_messages=True,ai_prefix="A",human_prefix="H" 35 | ) 36 | return memory 37 | 38 | def langchain_agent(memory,tools,llm): 39 | zero_shot_agent=initialize_agent( 40 | llm=llm, 41 | agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, 42 | tools=tools, 43 | #verbose=True, 44 | max_iteration=1, 45 | #return_intermediate_steps=True, 46 | handle_parsing_errors=True, 47 | memory=memory 48 | ) 49 | return zero_shot_agent -------------------------------------------------------------------------------- /03-rag-agent-bedrock/layers/common/python/db_utils.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import requests 3 | from boto3.dynamodb.conditions import Key 4 | 5 | 6 | def query_gd(key,table,keyvalue,Index_Name): 7 | resp = table.query( 8 | # Add the name of the index you want to use in your query. 9 | IndexName="jobnameindex", 10 | KeyConditionExpression=Key(key).eq(keyvalue), 11 | ) 12 | print(resp) 13 | return resp['Items'][0] 14 | 15 | def query(key,table,keyvalue): 16 | response = table.query( 17 | KeyConditionExpression=Key(key).eq(keyvalue) 18 | ) 19 | print(response) 20 | return response['Items'][0] 21 | 22 | def update_item(value,end,duration,table): 23 | try: 24 | response = table.update_item( 25 | Key={ 26 | "messages_id" : value 27 | }, 28 | UpdateExpression="set end=:newState, duration=:newState1", 29 | ExpressionAttributeValues={ 30 | ':newState': end, 31 | ':newState1': duration, 32 | }, 33 | ReturnValues="UPDATED_NEW") 34 | print (response) 35 | except Exception as e: 36 | print (e) 37 | else: 38 | return response 39 | 40 | def save_item_ddb(table,item): 41 | response = table.put_item(Item=item) 42 | return response 43 | 44 | def update_items_out(table,value,response_out,end_response): 45 | try: 46 | response = table.update_item( 47 | Key={ 48 | "messages_id" : value 49 | }, 50 | UpdateExpression="set response_out=:item1, end_response=:item2", 51 | ExpressionAttributeValues={ 52 | ':item1': response_out, 53 | ':item2': end_response, 54 | }, 55 | ReturnValues="UPDATED_NEW") 56 | print (response) 57 | except Exception as e: 58 | print (e) 59 | else: 60 | return response 61 | 62 | def update_item_session(table_name_session,value,session_time): 63 | try: 64 | response = table_name_session.update_item( 65 | Key={ 66 | "phone_number" : value 67 | }, 68 | UpdateExpression="set session_time=:item1", 69 | ExpressionAttributeValues={ 70 | ':item1': session_time 71 | }, 72 | ReturnValues="UPDATED_NEW") 73 | print (response) 74 | except Exception as e: 75 | print (e) 76 | else: 77 | return response 78 | 79 | -------------------------------------------------------------------------------- /03-rag-agent-bedrock/layers/common/python/file_utils.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import requests 3 | 4 | s3_resource = boto3.resource('s3') 5 | 6 | client_s3 = boto3.client('s3') 7 | 8 | def download_file(base_path,bucket, key, filename): 9 | print("Download file from s3://{}{}".format(bucket,key)) 10 | with open(base_path+filename, "wb") as data: 11 | client_s3.download_fileobj(bucket, key, data) 12 | print("Download file from s3://{}{}".format(bucket,key)) 13 | return True 14 | 15 | def upload_data_to_s3(bytes_data,bucket_name, s3_key): 16 | obj = s3_resource.Object(bucket_name, s3_key) 17 | obj.put(ACL='private', Body=bytes_data) 18 | s3_url = f"https://{bucket_name}.s3.amazonaws.com/{s3_key}" 19 | return s3_url 20 | 21 | def download_file_from_url(url): 22 | response = requests.get(url) 23 | if response.status_code == 200: 24 | return response.content 25 | else: 26 | return None 27 | 28 | def get_media_url(mediaId,whatsToken): 29 | 30 | URL = 'https://graph.facebook.com/v17.0/'+mediaId 31 | headers = {'Authorization': whatsToken} 32 | print("Requesting") 33 | response = requests.get(URL, headers=headers) 34 | responsejson = response.json() 35 | if('url' in responsejson): 36 | print("Responses: "+ str(responsejson)) 37 | return responsejson['url'] 38 | else: 39 | print("No URL returned") 40 | return None 41 | 42 | def get_whats_media(url,whatsToken): 43 | headers = {'Authorization': whatsToken} 44 | response = requests.get(url,headers=headers) 45 | if response.status_code == 200: 46 | return response.content 47 | else: 48 | return None 49 | 50 | def put_file(base_path,filename, bucket, key): 51 | with open(base_path+filename, "rb") as data: 52 | client_s3.upload_fileobj(data,bucket, key+filename) 53 | print("Put file in s3://{}{}{}".format(bucket,key,filename)) 54 | 55 | 56 | -------------------------------------------------------------------------------- /03-rag-agent-bedrock/layers/common/python/utils.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | from botocore.exceptions import ClientError 3 | import json 4 | secrets_client = boto3.client(service_name='secretsmanager') 5 | lambda_client = boto3.client('lambda') 6 | 7 | 8 | def get_config(secret_name): 9 | # Create a Secrets Manager client 10 | try: 11 | get_secret_value_response = secrets_client.get_secret_value( 12 | SecretId=secret_name 13 | ) 14 | except ClientError as e: 15 | if e.response['Error']['Code'] == 'DecryptionFailureException': 16 | # Secrets Manager can't decrypt the protected secret text using the provided KMS key. 17 | # Deal with the exception here, and/or rethrow at your discretion. 18 | raise e 19 | elif e.response['Error']['Code'] == 'InternalServiceErrorException': 20 | # An error occurred on the server side. 21 | # Deal with the exception here, and/or rethrow at your discretion. 22 | raise e 23 | elif e.response['Error']['Code'] == 'InvalidParameterException': 24 | # You provided an invalid value for a parameter. 25 | # Deal with the exception here, and/or rethrow at your discretion. 26 | raise e 27 | elif e.response['Error']['Code'] == 'InvalidRequestException': 28 | # You provided a parameter value that is not valid for the current state of the resource. 29 | # Deal with the exception here, and/or rethrow at your discretion. 30 | raise e 31 | elif e.response['Error']['Code'] == 'ResourceNotFoundException': 32 | # We can't find the resource that you asked for. 33 | # Deal with the exception here, and/or rethrow at your discretion. 34 | raise e 35 | else: 36 | # Decrypts secret using the associated KMS CMK. 37 | # Depending on whether the secret is a string or binary, one of these fields will be populated. 38 | if 'SecretString' in get_secret_value_response: 39 | secret = get_secret_value_response['SecretString'] 40 | else: 41 | secret = None 42 | return secret 43 | 44 | def normalize_phone(phone): 45 | ### Country specific changes required on phone numbers 46 | 47 | ### Mexico specific, remove 1 after 52 48 | if(phone[0:3]=='+52' and phone[3] == '1'): 49 | normalized = phone[0:3] + phone[4:] 50 | elif(phone[0:2]=='52' and phone[2] == '1'): 51 | normalized = phone[0:2] + phone[3:] 52 | else: 53 | normalized = phone 54 | return normalized 55 | ### End Mexico specific 56 | 57 | def get_file_category(mimeType): 58 | ## Possible {AUDIO, CONTACTS, DOCUMENT, IMAGE, TEXT, TEMPLATE, VIDEO, STICKER, LOCATION, INTERACTIVE, REACTION} 59 | if('application' in mimeType): return 'document' 60 | elif('image' in mimeType): return 'image' 61 | elif('audio' in mimeType): return 'audio' 62 | elif('video' in mimeType): return 'video' 63 | 64 | 65 | def build_response(status_code, json_content): 66 | return { 67 | 'statusCode': status_code, 68 | "headers": { 69 | "Content-Type": "text/html;charset=UTF-8", 70 | "charset": "UTF-8", 71 | "Access-Control-Allow-Origin": "*" 72 | }, 73 | 'body': json_content 74 | } 75 | 76 | def validate_healthcheck(event, WHATS_VERIFICATION_TOKEN ): 77 | if('queryStringParameters' in event and 'hub.challenge' in event['queryStringParameters']): 78 | print(event['queryStringParameters']) 79 | print("Token challenge") 80 | if(event['queryStringParameters']['hub.verify_token'] == WHATS_VERIFICATION_TOKEN): 81 | print("Token verified") 82 | print(event['queryStringParameters']['hub.challenge']) 83 | response = event['queryStringParameters']['hub.challenge'] 84 | else: 85 | response = '' 86 | else: 87 | print("Not challenge related") 88 | response = ' No key, no fun!' 89 | return response 90 | 91 | def whats_reply(whatsapp_out_lambda,phone, whats_token, phone_id, message, in_reply_to): 92 | 93 | print("WHATSAPP_OUT", in_reply_to, whats_token) 94 | 95 | try: 96 | 97 | response_2 = lambda_client.invoke( 98 | FunctionName = whatsapp_out_lambda, 99 | InvocationType = 'Event' ,#'RequestResponse', 100 | Payload = json.dumps({ 101 | 'phone': phone, 102 | 'whats_token': whats_token, 103 | 'phone_id': phone_id, 104 | 'message': message, 105 | 'in_reply_to': in_reply_to 106 | 107 | }) 108 | ) 109 | 110 | print(f'\nRespuesta:{response_2}') 111 | 112 | return response_2 113 | except ClientError as e: 114 | err = e.response 115 | error = err 116 | print(err.get("Error", {}).get("Code")) 117 | return f"Un error invocando {whatsapp_out_lambda}" -------------------------------------------------------------------------------- /03-rag-agent-bedrock/layers/project_layers.py: -------------------------------------------------------------------------------- 1 | import json 2 | from constructs import Construct 3 | 4 | from aws_cdk import ( 5 | aws_lambda 6 | 7 | ) 8 | 9 | 10 | class Layers(Construct): 11 | 12 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 13 | super().__init__(scope, construct_id, **kwargs) 14 | 15 | #layer con beautiful soup y requests 16 | bs4_requests = aws_lambda.LayerVersion( 17 | self, "Bs4Requests", code=aws_lambda.Code.from_asset("./layers/bs4_requests.zip"), 18 | compatible_runtimes = [ 19 | aws_lambda.Runtime.PYTHON_3_8, 20 | aws_lambda.Runtime.PYTHON_3_9, 21 | aws_lambda.Runtime.PYTHON_3_10, 22 | aws_lambda.Runtime.PYTHON_3_11], 23 | description = 'BeautifulSoup y Requests') 24 | 25 | self.bs4_requests = bs4_requests 26 | 27 | aiofile_transcribe = aws_lambda.LayerVersion( 28 | self, "aiofile-transcribe", code=aws_lambda.Code.from_asset("./layers/aiofile-amazon-transcribe.zip"), 29 | compatible_runtimes = [ 30 | aws_lambda.Runtime.PYTHON_3_8, 31 | aws_lambda.Runtime.PYTHON_3_9, 32 | aws_lambda.Runtime.PYTHON_3_10, 33 | aws_lambda.Runtime.PYTHON_3_11], 34 | description = 'aiofile y amazon-transcribe', layer_version_name = "aiofile-transcribe-streamig" 35 | ) 36 | 37 | self.aiofile_transcribe = aiofile_transcribe 38 | 39 | common = aws_lambda.LayerVersion( 40 | self, "common-layer", code=aws_lambda.Code.from_asset("./layers/common/"), 41 | compatible_runtimes = [ 42 | aws_lambda.Runtime.PYTHON_3_8, 43 | aws_lambda.Runtime.PYTHON_3_9, 44 | aws_lambda.Runtime.PYTHON_3_10, 45 | aws_lambda.Runtime.PYTHON_3_11], 46 | description = 'librerias adicionales', layer_version_name = "librerias-adicionales" 47 | ) 48 | 49 | self.common = common 50 | 51 | 52 | -------------------------------------------------------------------------------- /03-rag-agent-bedrock/rag_agent_bedrock/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/03-rag-agent-bedrock/rag_agent_bedrock/__init__.py -------------------------------------------------------------------------------- /03-rag-agent-bedrock/rag_agent_bedrock/ag_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "action_group_name": "GetPassengerInformation", 4 | "description": "useful for searching passenger data by their Code ID", 5 | "lambda_": "", 6 | "functions": { 7 | "name": "GetInformation", 8 | "description": "useful for searching passenger data by their Code ID", 9 | "parameters": [ 10 | { 11 | "name": "passenger_id", 12 | "type": "string", 13 | "description": "passenger code id, a sequence of 4 digits", 14 | "required": 1 15 | } 16 | ] 17 | } 18 | }, 19 | { 20 | "action_group_name": "AskTodayDAy", 21 | "description": "useful when you want to know today date and time", 22 | "lambda_": "", 23 | "functions": { 24 | "name": "GetTodayDateTime", 25 | "description": "useful when you want to know today date and time in DD/MM/YY HH:MM:SS format", 26 | "parameters": [ 27 | { 28 | "name": "date", 29 | "type": "string", 30 | "description": "date", 31 | "required": 0 32 | } 33 | ] 34 | } 35 | }, 36 | 37 | { 38 | "action_group_name": "CreateSupportTicket", 39 | "description": "Get or create support tickets for customers issues", 40 | "functions": { 41 | "name": "newTicket", 42 | "description": "Create a new support ticket. Use this when customer either have an issue with the service that cannot be resolved using only the documentation.", 43 | "parameters": [ 44 | { 45 | "name": "email", 46 | "type": "string", 47 | "description": "customer's email", 48 | "required": 1 49 | }, 50 | { 51 | "name": "name", 52 | "type": "string", 53 | "description": "passanger's full name", 54 | "required": 1 55 | }, 56 | { 57 | "name": "description", 58 | "type": "string", 59 | "description": "Customer issue in 1 paragraph. Be concise and clear.", 60 | "required": 1 61 | }, 62 | { 63 | "name": "passenger_id", 64 | "type": "string", 65 | "description": "passenger code id, a sequence of 4 digits", 66 | "required": 0 67 | }, 68 | { 69 | "name": "additional_data", 70 | "type": "string", 71 | "description": "additional data that could be useful", 72 | "required": 0 73 | } 74 | ] 75 | } 76 | }, 77 | { 78 | "action_group_name": "GetSupporTicketStatus", 79 | "description": "Status of a support ticket. Use this when customer needs to know the status of an existing ticket. The input is the ticket number (8 digit)", 80 | "lambda_": "", 81 | "functions": { 82 | "name": "getTicket", 83 | "description": "Use this when customer needs to know the status of an existing ticket", 84 | "parameters": [ 85 | { 86 | "name": "ticket_number", 87 | "type": "string", 88 | "description": "ticket ID, a sequence of 4 digits", 89 | "required": 1 90 | } 91 | ] 92 | } 93 | } 94 | ] -------------------------------------------------------------------------------- /03-rag-agent-bedrock/rag_agent_bedrock/agent_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "agent_name": "assistant-for-la-inventada-airlines-q-about-users-creates-queries-support-tickets", 3 | "description": "La Inventada Agent", 4 | "foundation_model": "anthropic.claude-3-5-sonnet-20240620-v1:0" 5 | } 6 | -------------------------------------------------------------------------------- /03-rag-agent-bedrock/rag_agent_bedrock/agent_prompt.txt: -------------------------------------------------------------------------------- 1 | You are an AI assistant for La Inventada Airlines, tasked with providing passenger information and support. Your role is to: 2 | 3 | 1. Access and deliver factual information about passengers' flight status, reservations, and other relevant details. 4 | 2. Offer helpful guidance and assistance to passengers. 5 | 3. Engage in casual conversation to create a friendly and positive customer experience. 6 | 4. Create support tickets when necessary and consult existing tickets. 7 | 8 | To begin, please follow these steps: 9 | 10 | 1. Greet the user warmly and introduce yourself as La Inventada Airlines' virtual assistant. 11 | 2. Ask for the passenger's ID or reservation number to access their information. 12 | 3. Once provided, acknowledge receipt of the information. 13 | 14 | Before responding to the passenger, consider: 15 | - What key information should I check first (e.g., flight status, check-in status, seat assignment)? 16 | - Are there any potential issues or special requirements I should be aware of? 17 | - What additional assistance might this passenger need based on their itinerary or status? 18 | - Is there an existing support ticket for this passenger that I should consult? 19 | 20 | After your consideration, provide the relevant information to the passenger. Be sure to: 21 | - Clearly communicate important details about their flight or reservation. 22 | - Offer proactive assistance for any potential issues you've identified. 23 | - Maintain a friendly and conversational tone throughout the interaction. 24 | - Consult any existing support tickets related to the passenger if applicable. 25 | 26 | If the passenger asks questions unrelated to their flight or reservation, engage in casual conversation while gently steering the discussion back to travel-related topics when appropriate. 27 | 28 | If you encounter a query or issue that is beyond your capabilities or requires human intervention: 29 | 1. Inform the passenger that you'll need to create a support ticket for further assistance. 30 | 2. Create a support ticket with all relevant details of the passenger's query or issue. 31 | 3. Provide the passenger with the ticket number and inform them about the next steps. 32 | 33 | Remember to always prioritize accuracy in factual information while maintaining a helpful and friendly demeanor. If you're unsure about any information or unable to resolve an issue, don't hesitate to create a support ticket. 34 | 35 | Always reply in the original user language. 36 | 37 | 38 | {{PASSENGER_DATA}} 39 | 40 | 41 | 42 | {{SUPPORT_TICKET_SYSTEM}} 43 | 44 | 45 | How may I assist you today? -------------------------------------------------------------------------------- /03-rag-agent-bedrock/rag_agent_bedrock/kb_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description_kb": "Documents regarding to help the passenger" 4 | } 5 | ] -------------------------------------------------------------------------------- /03-rag-agent-bedrock/rag_agent_bedrock/rag_agent_bedrock_stack.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | # Duration, 3 | Stack, 4 | aws_dynamodb as ddb, 5 | SecretValue, 6 | RemovalPolicy, 7 | aws_iam as iam, 8 | aws_bedrock as bedrock, 9 | aws_secretsmanager as secretsmanager, 10 | aws_ssm as ssm, 11 | 12 | ) 13 | from constructs import Construct 14 | 15 | from agent_bedrock import CreateAgentWithKA 16 | from databases import Tables 17 | from lambdas import (Lambdas,DynamodbWithSampleDataStack) 18 | from agent_role import CreateAgentRole 19 | import os 20 | import json 21 | 22 | from get_param import get_string_param 23 | 24 | # ############ 25 | # The provided code block is a Python function called 26 | #create_kb_property 27 | # that takes a kb_data.json 28 | # parameter, which is likely a list of dictionaries 29 | # representing knowledge base data. 30 | # ########### 31 | def create_kb_property(kb_data): 32 | kb_group_properties = [] 33 | for n in kb_data: 34 | kb_group_property = bedrock.CfnAgent.AgentKnowledgeBaseProperty( 35 | description=n["description_kb"], 36 | knowledge_base_id=n["knowledge_base_id"], 37 | ) 38 | kb_group_properties.append(kb_group_property) 39 | return kb_group_properties 40 | 41 | # ####### 42 | # The provided code block is a Python function called 43 | # agent_action_group_property that takes an ag_data.json 44 | # parameter, which is likely a list of dictionaries 45 | # representing agent action group data. 46 | # ####### 47 | 48 | def agent_action_group_property(ag_data): 49 | agent_action_group_properties = [] 50 | agent_action_group_property = bedrock.CfnAgent.AgentActionGroupProperty( 51 | action_group_name="askinuput", 52 | parent_action_group_signature="AMAZON.UserInput", 53 | #skip_resource_in_use_check_on_delete=False 54 | ) 55 | agent_action_group_properties.append(agent_action_group_property) 56 | for n in ag_data: 57 | parameters = {} 58 | for p in n["functions"]["parameters"]: 59 | parameters[p["name"]] = bedrock.CfnAgent.ParameterDetailProperty( 60 | type=p["type"], 61 | 62 | # the properties below are optional 63 | description=p["description"], 64 | required=bool(p["required"]) 65 | ) 66 | 67 | agent_action_group_property = bedrock.CfnAgent.AgentActionGroupProperty( 68 | action_group_name=n["action_group_name"], 69 | 70 | # the properties below are optional 71 | action_group_executor=bedrock.CfnAgent.ActionGroupExecutorProperty( 72 | lambda_=n["lambda_"] 73 | ), 74 | 75 | action_group_state="ENABLED", 76 | #description=n["description"], 77 | function_schema=bedrock.CfnAgent.FunctionSchemaProperty( 78 | functions=[bedrock.CfnAgent.FunctionProperty( 79 | name=n["functions"]["name"], 80 | # the properties below are optional 81 | description=n["functions"]["description"], 82 | parameters=parameters 83 | )] 84 | ), 85 | #parent_action_group_signature="AMAZON.UserInput", 86 | skip_resource_in_use_check_on_delete=False 87 | ) 88 | agent_action_group_properties.append(agent_action_group_property) 89 | return agent_action_group_properties 90 | 91 | REMOVAL_POLICY = RemovalPolicy.DESTROY 92 | TABLE_CONFIG = dict (removal_policy=REMOVAL_POLICY, billing_mode= ddb.BillingMode.PAY_PER_REQUEST) 93 | AudioKeyName = "audio-from-whatsapp" 94 | TextBucketName = "text-to-whatsapp" 95 | DISPLAY_PHONE_NUMBER = 'YOU-NUMBER' 96 | 97 | ssm_kb_id = get_string_param("/pgvector/kb_id") 98 | 99 | class RagAgentBedrockStack(Stack): 100 | 101 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 102 | super().__init__(scope, construct_id, **kwargs) 103 | 104 | # The code that defines your stack goes here 105 | stk = Stack.of(self) 106 | ACCOUNT_ID = stk.account 107 | REGION = self.region 108 | 109 | ENV_KEY_NAME = "date" 110 | env_key_sec_global = "phone_number" 111 | file_path_ag_data = './rag_agent_bedrock/ag_data.json' 112 | file_path_agent_data = './rag_agent_bedrock/agent_data.json' 113 | file_path_kb_data = './rag_agent_bedrock/kb_data.json' 114 | agent_prompt = './rag_agent_bedrock/agent_prompt.txt' 115 | 116 | Fn = Lambdas(self,'Fn',ACCOUNT_ID) 117 | Tbl = Tables(self, 'Tbl') 118 | 119 | with open(file_path_agent_data, 'r') as file: 120 | agent_data = json.load(file) 121 | 122 | with open(agent_prompt, 'r', encoding='utf-8') as f: 123 | improved_prompt = f.read() 124 | 125 | agent_data["agent_instruction"] = improved_prompt 126 | 127 | #ag_data exists 128 | if os.path.exists(file_path_ag_data): 129 | with open(file_path_ag_data, 'r') as file: 130 | ag_data = json.load(file) 131 | #add lambda arn to action group data 132 | ag_data[0]["lambda_"] = Fn.dynamodb_query_passanger.function_arn 133 | ag_data[1]["lambda_"] = Fn.ask_date.function_arn 134 | ag_data[2]["lambda_"] = Fn.dynamodb_put_item_random_key.function_arn 135 | ag_data[3]["lambda_"] = Fn.dynamodb_query_ticket.function_arn 136 | 137 | #kb_data exists 138 | if os.path.exists(file_path_kb_data): 139 | with open(file_path_kb_data, 'r') as file: 140 | kb_data = json.load(file) 141 | kb_data[0]["knowledge_base_id"] = ssm_kb_id 142 | else: 143 | kb_data = None 144 | 145 | Tbl.passangerTable.grant_full_access(Fn.dynamodb_put_item) 146 | 147 | # Load data into table 148 | DynamodbWithSampleDataStack( 149 | self, "pasanger-qa-base", 150 | lambda_function=Fn.dynamodb_put_item, 151 | table= Tbl.passangerTable, 152 | file_name = "dataset.csv" 153 | ) 154 | 155 | Fn.dynamodb_query_passanger.add_environment(key='TABLE_NAME', value=Tbl.passangerTable.table_name) 156 | Fn.dynamodb_query_passanger.add_environment(key='ENV_KEY_NAME', value="passenger_id") 157 | Tbl.passangerTable.grant_full_access(Fn.dynamodb_query_passanger) 158 | 159 | Fn.dynamodb_query_ticket.add_environment(key='TABLE_NAME', value=Tbl.ticketTable.table_name) 160 | Fn.dynamodb_query_ticket.add_environment(key='ENV_KEY_NAME', value="ticket_number") 161 | Tbl.ticketTable.grant_full_access(Fn.dynamodb_query_ticket) 162 | 163 | Fn.dynamodb_put_item_random_key.add_environment(key='TABLE_NAME', value=Tbl.ticketTable.table_name) 164 | Fn.dynamodb_put_item_random_key.add_environment(key='ENV_KEY_NAME', value="ticket_number") 165 | Tbl.ticketTable.grant_full_access(Fn.dynamodb_put_item_random_key) 166 | 167 | agent_name = "assistant-for-la-inventada-airlines-q-about-users-creates-queries-support-tickets" 168 | print("Create La Inventada Agent") 169 | agent_action_group_properties = agent_action_group_property(ag_data) 170 | agent_knowledge_base_property =create_kb_property(kb_data) 171 | 172 | # Agent Execution Role 173 | agent_resource_role = CreateAgentRole(self, "role") 174 | 175 | agent = CreateAgentWithKA(self, "agentwithbooth", agent_name, agent_data["foundation_model"], agent_data["agent_instruction"], agent_data["description"], agent_knowledge_base_property, agent_action_group_properties, agent_resource_role.arn) 176 | 177 | agent_id = agent.cfn_agent.attr_agent_id 178 | 179 | #https://docs.aws.amazon.com/cdk/api/v2/python/aws_cdk.aws_bedrock/CfnAgentAlias.html 180 | 181 | agent_alias = bedrock.CfnAgentAlias(self, "MyCfnAgentAlias", 182 | agent_alias_name="AgenteLaInventada", 183 | agent_id=agent_id, 184 | # the properties below are optional 185 | description="Agente La Inventada", 186 | 187 | ) 188 | 189 | ssm.StringParameter( self, "agentid_ssm", parameter_name=f"/agentvalue/agent_id", string_value=agent_id) 190 | ssm.StringParameter( self, "aliasid_ssm", parameter_name=f"/agentvalue/alias_id", string_value=agent_alias.attr_agent_alias_id) 191 | -------------------------------------------------------------------------------- /03-rag-agent-bedrock/rag_agent_bedrock/utils_agent.py: -------------------------------------------------------------------------------- 1 | def create_kb_property(kb_data): 2 | kb_group_properties = [] 3 | for n in kb_data: 4 | kb_group_property = bedrock.CfnAgent.AgentKnowledgeBaseProperty( 5 | description=n["description_kb"], 6 | knowledge_base_id=n["knowledge_base_id"], 7 | ) 8 | kb_group_properties.append(kb_group_property) 9 | return kb_group_properties 10 | def agent_action_group_property(ag_data): 11 | agent_action_group_properties = [] 12 | agent_action_group_property = bedrock.CfnAgent.AgentActionGroupProperty( 13 | action_group_name="askinuput", 14 | parent_action_group_signature="AMAZON.UserInput", 15 | #skip_resource_in_use_check_on_delete=False 16 | ) 17 | agent_action_group_properties.append(agent_action_group_property) 18 | for n in ag_data: 19 | parameters = {} 20 | for p in n["functions"]["parameters"]: 21 | parameters[p["name"]] = bedrock.CfnAgent.ParameterDetailProperty( 22 | type=p["type"], 23 | 24 | # the properties below are optional 25 | description=p["description"], 26 | required=bool(p["required"]) 27 | ) 28 | 29 | agent_action_group_property = bedrock.CfnAgent.AgentActionGroupProperty( 30 | action_group_name=n["action_group_name"], 31 | 32 | # the properties below are optional 33 | action_group_executor=bedrock.CfnAgent.ActionGroupExecutorProperty( 34 | lambda_=n["lambda_"] 35 | ), 36 | 37 | action_group_state="ENABLED", 38 | #description=n["description"], 39 | function_schema=bedrock.CfnAgent.FunctionSchemaProperty( 40 | functions=[bedrock.CfnAgent.FunctionProperty( 41 | name=n["functions"]["name"], 42 | # the properties below are optional 43 | description=n["functions"]["description"], 44 | parameters=parameters 45 | )] 46 | ), 47 | #parent_action_group_signature="AMAZON.UserInput", 48 | skip_resource_in_use_check_on_delete=False 49 | ) 50 | agent_action_group_properties.append(agent_action_group_property) 51 | return agent_action_group_properties 52 | -------------------------------------------------------------------------------- /03-rag-agent-bedrock/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest==6.2.5 2 | -------------------------------------------------------------------------------- /03-rag-agent-bedrock/requirements.txt: -------------------------------------------------------------------------------- 1 | aws-cdk-lib==2.146.0 2 | constructs>=10.0.0,<11.0.0 3 | boto3 -------------------------------------------------------------------------------- /03-rag-agent-bedrock/reschedule_flight.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/03-rag-agent-bedrock/reschedule_flight.gif -------------------------------------------------------------------------------- /03-rag-agent-bedrock/source.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | rem The sole purpose of this script is to make the command 4 | rem 5 | rem source .venv/bin/activate 6 | rem 7 | rem (which activates a Python virtualenv on Linux or Mac OS X) work on Windows. 8 | rem On Windows, this command just runs this batch file (the argument is ignored). 9 | rem 10 | rem Now we don't need to document a Windows command for activating a virtualenv. 11 | 12 | echo Executing .venv\Scripts\activate.bat for you 13 | .venv\Scripts\activate.bat 14 | -------------------------------------------------------------------------------- /03-rag-agent-bedrock/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/03-rag-agent-bedrock/tests/__init__.py -------------------------------------------------------------------------------- /03-rag-agent-bedrock/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/03-rag-agent-bedrock/tests/unit/__init__.py -------------------------------------------------------------------------------- /03-rag-agent-bedrock/tests/unit/test_rag_agent_bedrock_stack.py: -------------------------------------------------------------------------------- 1 | import aws_cdk as core 2 | import aws_cdk.assertions as assertions 3 | 4 | from rag_agent_bedrock.rag_agent_bedrock_stack import RagAgentBedrockStack 5 | 6 | # example tests. To run these tests, uncomment this file along with the example 7 | # resource in rag_agent_bedrock/rag_agent_bedrock_stack.py 8 | def test_sqs_queue_created(): 9 | app = core.App() 10 | stack = RagAgentBedrockStack(app, "rag-agent-bedrock") 11 | template = assertions.Template.from_stack(stack) 12 | 13 | # template.has_resource_properties("AWS::SQS::Queue", { 14 | # "VisibilityTimeout": 300 15 | # }) 16 | -------------------------------------------------------------------------------- /03-rag-agent-bedrock/ticket.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/03-rag-agent-bedrock/ticket.gif -------------------------------------------------------------------------------- /04-whatsapp-app/.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | package-lock.json 3 | __pycache__ 4 | .pytest_cache 5 | .venv 6 | *.egg-info 7 | 8 | # CDK asset staging directory 9 | .cdk.staging 10 | cdk.out 11 | -------------------------------------------------------------------------------- /04-whatsapp-app/apis/__init__.py: -------------------------------------------------------------------------------- 1 | from apis.webhooks import WebhookApi -------------------------------------------------------------------------------- /04-whatsapp-app/apis/webhooks.py: -------------------------------------------------------------------------------- 1 | 2 | from aws_cdk import ( 3 | aws_apigateway as apg, 4 | Stack 5 | ) 6 | 7 | from constructs import Construct 8 | 9 | 10 | 11 | class WebhookApi(Construct): 12 | 13 | def __init__(self, scope: Construct, construct_id: str,lambdas, **kwargs) -> None: 14 | super().__init__(scope, construct_id, **kwargs) 15 | 16 | api = apg.RestApi(self, "myapi") 17 | api.root.add_cors_preflight(allow_origins=["*"], allow_methods=["GET", "POST"], allow_headers=["*"]) 18 | 19 | cloudapi = api.root.add_resource("cloudapi",default_integration=apg.LambdaIntegration(lambdas.whatsapp_in, allow_test_invoke=False)) 20 | 21 | cloudapi.add_method("GET") 22 | 23 | cloudapi.add_method("POST") 24 | -------------------------------------------------------------------------------- /04-whatsapp-app/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | 4 | import aws_cdk as cdk 5 | 6 | from whatsapp_app.whatsapp_app_stack import WhatsappAppStack 7 | 8 | 9 | app = cdk.App() 10 | WhatsappAppStack(app, "WhatsappAppStack", 11 | # If you don't specify 'env', this stack will be environment-agnostic. 12 | # Account/Region-dependent features and context lookups will not work, 13 | # but a single synthesized template can be deployed anywhere. 14 | 15 | # Uncomment the next line to specialize this stack for the AWS Account 16 | # and Region that are implied by the current CLI configuration. 17 | 18 | #env=cdk.Environment(account=os.getenv('CDK_DEFAULT_ACCOUNT'), region=os.getenv('CDK_DEFAULT_REGION')), 19 | 20 | # Uncomment the next line if you know exactly what Account and Region you 21 | # want to deploy the stack to. */ 22 | 23 | #env=cdk.Environment(account='123456789012', region='us-east-1'), 24 | 25 | # For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html 26 | ) 27 | 28 | app.synth() 29 | -------------------------------------------------------------------------------- /04-whatsapp-app/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "python3 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 | "**/__pycache__", 14 | "tests" 15 | ] 16 | }, 17 | "context": { 18 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 19 | "@aws-cdk/core:checkSecretUsage": true, 20 | "@aws-cdk/core:target-partitions": [ 21 | "aws", 22 | "aws-cn" 23 | ], 24 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 25 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 26 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 27 | "@aws-cdk/aws-iam:minimizePolicies": true, 28 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 29 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 30 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 31 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 32 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 33 | "@aws-cdk/core:enablePartitionLiterals": true, 34 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 35 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 36 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 37 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 38 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 39 | "@aws-cdk/aws-route53-patters:useCertificate": true, 40 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 41 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 42 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 43 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 44 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 45 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 46 | "@aws-cdk/aws-redshift:columnId": true, 47 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 48 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 49 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 50 | "@aws-cdk/aws-kms:aliasNameRef": true, 51 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 52 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 53 | "@aws-cdk/aws-efs:denyAnonymousAccess": true, 54 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, 55 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, 56 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, 57 | "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, 58 | "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, 59 | "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, 60 | "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, 61 | "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, 62 | "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, 63 | "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, 64 | "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, 65 | "@aws-cdk/aws-eks:nodegroupNameAttribute": true, 66 | "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /04-whatsapp-app/databases/__init__.py: -------------------------------------------------------------------------------- 1 | from databases.databases import Tables -------------------------------------------------------------------------------- /04-whatsapp-app/databases/databases.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | RemovalPolicy, 3 | aws_dynamodb as ddb 4 | ) 5 | from constructs import Construct 6 | 7 | 8 | REMOVAL_POLICY = RemovalPolicy.DESTROY 9 | 10 | TABLE_CONFIG = dict (removal_policy=REMOVAL_POLICY, billing_mode= ddb.BillingMode.PAY_PER_REQUEST) 11 | 12 | 13 | class Tables(Construct): 14 | 15 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 16 | super().__init__(scope, construct_id, **kwargs) 17 | 18 | self.whatsapp_MetaData = ddb.Table( 19 | self, "whatsapp-MetaData", 20 | partition_key=ddb.Attribute(name="messages_id", type=ddb.AttributeType.STRING), 21 | stream=ddb.StreamViewType.NEW_AND_OLD_IMAGES 22 | ) 23 | 24 | #self.whatsapp_MetaData_follow = ddb.Table( 25 | # self, "whatsapp-MetaData-follow", 26 | # partition_key=ddb.Attribute(name="messages_id", type=ddb.AttributeType.STRING), 27 | # stream=ddb.StreamViewType.NEW_AND_OLD_IMAGES 28 | #) 29 | 30 | #self.user_sesion_metadata = ddb.Table( 31 | # self, "user_metadata", 32 | # partition_key=ddb.Attribute(name="phone_number", type=ddb.AttributeType.STRING), 33 | # **TABLE_CONFIG) 34 | -------------------------------------------------------------------------------- /04-whatsapp-app/get_param.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | ssm = boto3.client('ssm') 3 | 4 | def get_string_param(parameter_name): 5 | response = ssm.get_parameter(Name=parameter_name) 6 | parameter = response.get('Parameter') 7 | if parameter: 8 | return parameter.get('Value') 9 | else: 10 | return None -------------------------------------------------------------------------------- /04-whatsapp-app/imagenes/1_step.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/04-whatsapp-app/imagenes/1_step.jpg -------------------------------------------------------------------------------- /04-whatsapp-app/imagenes/2_1_step.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/04-whatsapp-app/imagenes/2_1_step.jpg -------------------------------------------------------------------------------- /04-whatsapp-app/imagenes/2_2_step.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/04-whatsapp-app/imagenes/2_2_step.jpg -------------------------------------------------------------------------------- /04-whatsapp-app/imagenes/2_step.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/04-whatsapp-app/imagenes/2_step.jpg -------------------------------------------------------------------------------- /04-whatsapp-app/imagenes/3_step.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/04-whatsapp-app/imagenes/3_step.jpg -------------------------------------------------------------------------------- /04-whatsapp-app/imagenes/QA.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/04-whatsapp-app/imagenes/QA.gif -------------------------------------------------------------------------------- /04-whatsapp-app/imagenes/arquitectura.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/04-whatsapp-app/imagenes/arquitectura.jpg -------------------------------------------------------------------------------- /04-whatsapp-app/imagenes/deployment_time.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/04-whatsapp-app/imagenes/deployment_time.jpg -------------------------------------------------------------------------------- /04-whatsapp-app/imagenes/flow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/04-whatsapp-app/imagenes/flow.jpg -------------------------------------------------------------------------------- /04-whatsapp-app/imagenes/gif_01.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/04-whatsapp-app/imagenes/gif_01.gif -------------------------------------------------------------------------------- /04-whatsapp-app/imagenes/invoke_url.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/04-whatsapp-app/imagenes/invoke_url.jpg -------------------------------------------------------------------------------- /04-whatsapp-app/imagenes/passanger_information.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/04-whatsapp-app/imagenes/passanger_information.gif -------------------------------------------------------------------------------- /04-whatsapp-app/imagenes/secret.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/04-whatsapp-app/imagenes/secret.png -------------------------------------------------------------------------------- /04-whatsapp-app/imagenes/voice_note.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/04-whatsapp-app/imagenes/voice_note.gif -------------------------------------------------------------------------------- /04-whatsapp-app/imagenes/webhook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/04-whatsapp-app/imagenes/webhook.png -------------------------------------------------------------------------------- /04-whatsapp-app/imagenes/whaytsapp_number.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/04-whatsapp-app/imagenes/whaytsapp_number.jpg -------------------------------------------------------------------------------- /04-whatsapp-app/lambdas/__init__.py: -------------------------------------------------------------------------------- 1 | from lambdas.project_lambdas import Lambdas -------------------------------------------------------------------------------- /04-whatsapp-app/lambdas/code/agent_bedrock/lambda_function.py: -------------------------------------------------------------------------------- 1 | ##################################################### 2 | ## This function queries anthropic.claude-3-sonnet ## 3 | ##################################################### 4 | 5 | import json 6 | import boto3 7 | import os 8 | import time 9 | import base64 10 | import sys 11 | 12 | bedrock_agent_client = boto3.client("bedrock-agent-runtime") 13 | agent_id = os.environ.get('ENV_AGENT_ID') 14 | agent_alias_id = os.environ.get('ENV_ALIAS_ID') 15 | whatsapp_out_lambda = os.environ.get('WHATSAPP_OUT') 16 | 17 | from utils import whats_reply 18 | 19 | 20 | def invoke_agent(agent_id, agent_alias_id, session_id, prompt): 21 | """ 22 | Sends a prompt for the agent to process and respond to. 23 | 24 | :param agent_id: The unique identifier of the agent to use. 25 | :param agent_alias_id: The alias of the agent to use. 26 | :param session_id: The unique identifier of the session. Use the same value across requests 27 | to continue the same conversation. 28 | :param prompt: The prompt that you want Claude to complete. 29 | :return: Inference response from the model. 30 | """ 31 | try: 32 | # Note: The execution time depends on the foundation model, complexity of the agent, 33 | # and the length of the prompt. In some cases, it can take up to a minute or more to 34 | # generate a response. 35 | response = bedrock_agent_client.invoke_agent( 36 | agentId=agent_id, 37 | agentAliasId=agent_alias_id, 38 | sessionId=session_id, 39 | inputText=prompt, 40 | ) 41 | print("response: ",response) 42 | 43 | completion = "" 44 | 45 | for event in response.get("completion"): 46 | chunk = event["chunk"] 47 | completion = completion + chunk["bytes"].decode() 48 | 49 | except: 50 | print(f"Couldn't invoke agent.") 51 | raise 52 | 53 | return completion 54 | 55 | def lambda_handler(event, context): 56 | print (event) 57 | 58 | prompt = event['whats_message'] 59 | print('REQUEST RECEIVED:', event) 60 | print('REQUEST CONTEXT:', context) 61 | print("PROMPT: ",prompt) 62 | 63 | try: 64 | whats_token = event['whats_token'] 65 | messages_id = event['messages_id'] 66 | phone = event['phone'] 67 | phone_id = event['phone_id'] 68 | 69 | completion = invoke_agent(agent_id, agent_alias_id, phone.replace("+",""), prompt) 70 | 71 | print(completion) 72 | 73 | whats_reply(whatsapp_out_lambda,phone, whats_token, phone_id, f"{completion}", messages_id) 74 | 75 | return({"body":completion}) 76 | 77 | except Exception as error: 78 | print('FAILED!', error) 79 | return({"body":"Cuek! I dont know"}) 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /04-whatsapp-app/lambdas/code/audio_job_transcriptor/lambda_function.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import os 3 | 4 | import time 5 | import sys 6 | import datetime 7 | import uuid 8 | from botocore.exceptions import ClientError 9 | import re 10 | 11 | 12 | from file_utils import( get_media_url , get_whats_media,put_file) 13 | 14 | base_path="/tmp/" 15 | 16 | SOURCE_LANG_CODE = os.environ.get('SOURCE_LANG_CODE') 17 | 18 | BucketName = os.environ.get('BucketName') 19 | AudioKeyName = os.environ.get('AudioKeyName') 20 | TextBucketName = os.environ.get('TextBucketName') 21 | 22 | table_name_active_connections = os.environ.get('whatsapp_MetaData') 23 | key_name_active_connections = "messages_id" 24 | 25 | dynamodb_resource=boto3.resource('dynamodb') 26 | table = dynamodb_resource.Table(table_name_active_connections) 27 | key_name_active_connections = "messages_id" 28 | 29 | transcribe_client = boto3.client('transcribe') 30 | client_s3 = boto3.client('s3') 31 | 32 | #https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/transcribe/client/start_transcription_job.html 33 | 34 | def db_update_item(value,jobName,now): 35 | print("Update jobname") 36 | try: 37 | response = table.update_item( 38 | Key={ 39 | "messages_id" : value 40 | }, 41 | UpdateExpression="set jobName=:item1, now=:item2", 42 | ExpressionAttributeValues={ 43 | ':item1': jobName, 44 | ':item2': now, 45 | }, 46 | ReturnValues="UPDATED_NEW") 47 | print (response) 48 | except Exception as e: 49 | print (e) 50 | else: 51 | return response 52 | 53 | 54 | def start_job_transciptor (jobName,s3Path_in,OutputKey,codec): 55 | print(s3Path_in) 56 | response = transcribe_client.start_transcription_job( 57 | TranscriptionJobName=jobName, 58 | #LanguageCode='es-US', 59 | IdentifyLanguage=True, 60 | MediaFormat=codec, 61 | Media={ 62 | 'MediaFileUri': s3Path_in 63 | }, 64 | OutputBucketName = BucketName, 65 | OutputKey=OutputKey 66 | ) 67 | TranscriptionJobName = response['TranscriptionJob']['TranscriptionJobName'] 68 | job = transcribe_client.get_transcription_job(TranscriptionJobName=TranscriptionJobName) 69 | job_status = job['TranscriptionJob']['TranscriptionJobStatus'] 70 | 71 | print("Processing....") 72 | print("Print job_status ....",job_status) 73 | print("TranscriptionJobName : {}".format(TranscriptionJobName)) 74 | 75 | 76 | def lambda_handler(event, context): 77 | 78 | print(event) 79 | 80 | start = int( time.time() ) 81 | whats_message = event['whats_message'] 82 | 83 | whats_token = event['whats_token'] 84 | messages_id = event['messages_id'] 85 | phone = event['phone'] 86 | phone_id = event['phone_id'] 87 | 88 | messageType = whats_message['type'] 89 | 90 | fileType = whats_message[messageType]['mime_type'] 91 | fileExtension = fileType.split('/')[-1] 92 | fileId = whats_message[messageType]['id'] 93 | fileName = f'{fileId}.{fileExtension}' 94 | fileUrl = get_media_url(fileId, whats_token) 95 | 96 | mime_type = fileType.split(";")[0] 97 | codec = mime_type.split("/")[-1] 98 | print(codec) 99 | 100 | if not fileUrl: return 101 | 102 | fileContents = get_whats_media(fileUrl,whats_token) 103 | fileSize = sys.getsizeof(fileContents) - 33 ## Removing BYTES overhead 104 | 105 | 106 | print(f"messageType:{messageType}") 107 | print(f"fileType:{fileType}") 108 | print(f"fileName:{fileName}") 109 | print(f"fileId:{fileId}") 110 | print(f"fileUrl:{fileUrl}") 111 | print("Size downloaded:" + str(fileSize)) 112 | 113 | print ("Ready To TRANSCRIPTION!") 114 | 115 | #audio_decoded = base64.b64decode(fileContents) 116 | 117 | now = int( time.time() ) 118 | 119 | LOCAL_FILE = f"audio_{fileId}.{codec}" 120 | 121 | with open(f"{base_path}{LOCAL_FILE}", "wb") as binary_file: 122 | binary_file.write(fileContents) 123 | 124 | put_file(base_path,LOCAL_FILE, BucketName, AudioKeyName+"/") 125 | 126 | s3Path_in = "s3://" + BucketName + "/" + AudioKeyName +"/"+LOCAL_FILE 127 | 128 | jobName = fileId 129 | 130 | bucket_key_out = TextBucketName +"/" + f"texto_{fileId}" 131 | 132 | start_job_transciptor (jobName,s3Path_in,bucket_key_out,codec) 133 | 134 | value = whats_message['id'].strip().replace(" ","") 135 | jobName= jobName.strip().replace(" ","") 136 | print(value) 137 | print(jobName) 138 | print(now) 139 | db_update_item(value,jobName,now) 140 | 141 | 142 | return True 143 | 144 | -------------------------------------------------------------------------------- /04-whatsapp-app/lambdas/code/process_stream/lambda_function.py: -------------------------------------------------------------------------------- 1 | #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 | ##++++++ Amazon Lambda Function for processing WhatsApp incoming messages +++++ 3 | ## Updated to Whatsapp API v14 4 | #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 | 6 | import boto3 7 | from boto3.dynamodb.types import TypeDeserializer 8 | import decimal 9 | import json 10 | import os 11 | 12 | import requests 13 | from botocore.exceptions import ClientError 14 | 15 | lambda_client = boto3.client('lambda') 16 | 17 | from file_utils import( get_media_url ) 18 | 19 | from utils import (build_response,whats_reply) 20 | 21 | SUPPORTED_FILE_TYPES = ['text/csv','image/png','image/jpeg','application/pdf'] 22 | whatsapp_out_lambda = os.environ.get('WHATSAPP_OUT') 23 | 24 | 25 | def ddb_deserialize(r, type_deserializer = TypeDeserializer()): 26 | return type_deserializer.deserialize({"M": r}) 27 | 28 | class DecimalEncoder(json.JSONEncoder): 29 | def default(self, o): 30 | if isinstance(o, decimal.Decimal): 31 | if o % 1 > 0: 32 | return float(o) 33 | else: 34 | return int(o) 35 | return super(DecimalEncoder, self).default(o) 36 | 37 | def lambda_handler(event, context): 38 | print (event) 39 | 40 | for rec in event['Records']: 41 | 42 | try: 43 | entry = json.loads(json.dumps(ddb_deserialize(rec['dynamodb']['NewImage']), cls=DecimalEncoder)) 44 | messages_id = entry["messages_id"] 45 | event_name = rec['eventName'] 46 | print(event_name) 47 | 48 | if event_name == "INSERT": 49 | print("messages_id: ", messages_id) 50 | print("entry: ",entry) 51 | WHATS_TOKEN = entry["whats_token"] 52 | 53 | for change in entry['changes']: 54 | print("Iterating change") 55 | print(change) 56 | ## Skipping as no contact info was relevant. 57 | if('contacts' not in change['value']): 58 | continue 59 | 60 | whats_message = change['value']['messages'][0] 61 | 62 | phone_id = change['value']['metadata']['phone_number_id'] 63 | name = change['value']['contacts'][0]['profile']['name'] 64 | phone = '+' + str(whats_message['from']) 65 | channel = 'whatsapp' 66 | 67 | ##Define message type 68 | messageType =whats_message['type'] 69 | if(messageType == 'text'): 70 | message = whats_message['text']['body'] 71 | process_text(message, WHATS_TOKEN,phone,phone_id,messages_id) 72 | elif(messageType == 'image'): 73 | #message = whats_message['image']['body'] 74 | #process_image(whats_message, WHATS_TOKEN,phone,phone_id,messages_id,messageType) 75 | text = "I am currently not able to process images." 76 | whats_reply(whatsapp_out_lambda,phone, WHATS_TOKEN, phone_id, text, messages_id) 77 | 78 | 79 | # Agregar para respuestas a boton! 80 | elif(messageType == 'button'): 81 | message =whats_message['button']['text'] 82 | 83 | elif(messageType == 'audio'): 84 | #processed_audio = process_audio(whats_message, WHATS_TOKEN,phone,systemNumber) 85 | processed_job_audio = star_job_audio(whats_message, WHATS_TOKEN,phone,phone_id,messages_id) 86 | 87 | message = "Procesando.." 88 | else: 89 | message = 'Attachment' 90 | fileType = whats_message[messageType]['mime_type'] 91 | fileName = whats_message[messageType].get('filename',phone + '.'+fileType.split("/")[1]) 92 | fileId = whats_message[messageType]['id'] 93 | fileUrl = get_media_url(fileId,WHATS_TOKEN) 94 | 95 | print(fileType) 96 | print(message, messageType, name, phone_id) 97 | print(build_response (200,json.dumps('All good!'))) 98 | else: 99 | print("no New INSERT") 100 | except: 101 | print("no New Image") 102 | 103 | return True 104 | 105 | def process_image(whats_message, whats_token,phone,phone_id,messages_id,messageType): 106 | 107 | print("text", whats_message, whats_token) 108 | LAMBDA_AGENT_IMAGE = os.environ['ENV_LAMBDA_AGENT_IMAGE'] 109 | 110 | try: 111 | 112 | response_3 = lambda_client.invoke( 113 | FunctionName = LAMBDA_AGENT_IMAGE, 114 | InvocationType = 'Event' ,#'RequestResponse', 115 | Payload = json.dumps({ 116 | 'whats_message': whats_message, 117 | 'whats_token': whats_token, 118 | 'phone': phone, 119 | 'phone_id': phone_id, 120 | 'messages_id': messages_id, 121 | 'type': messageType 122 | 123 | }) 124 | ) 125 | 126 | print(f'\nRespuesta:{response_3}') 127 | 128 | return response_3 129 | 130 | except ClientError as e: 131 | err = e.response 132 | error = err 133 | print(err.get("Error", {}).get("Code")) 134 | return f"Un error invocando {LAMBDA_AGENT_IMAGE}" 135 | 136 | def process_text(whats_message, whats_token,phone,phone_id,messages_id): 137 | 138 | print("text", whats_message, whats_token) 139 | ENV_AGENT_BEDROCK = os.environ['ENV_AGENT_BEDROCK'] 140 | 141 | try: 142 | 143 | response_3 = lambda_client.invoke( 144 | FunctionName = ENV_AGENT_BEDROCK, 145 | InvocationType = 'Event' ,#'RequestResponse', 146 | Payload = json.dumps({ 147 | 'whats_message': whats_message, 148 | 'whats_token': whats_token, 149 | 'phone': phone, 150 | 'phone_id': phone_id, 151 | 'messages_id': messages_id 152 | 153 | }) 154 | ) 155 | 156 | print(f'\nRespuesta:{response_3}') 157 | 158 | return response_3 159 | 160 | except ClientError as e: 161 | err = e.response 162 | error = err 163 | print(err.get("Error", {}).get("Code")) 164 | return f"Un error invocando {ENV_AGENT_BEDROCK}" 165 | 166 | 167 | def star_job_audio(whats_message, whats_token,phone,phone_id,messages_id): 168 | 169 | print("audio", whats_message, whats_token) 170 | JOB_TRANSCRIPTOR_LAMBDA = os.environ['JOB_TRANSCRIPTOR_LAMBDA'] 171 | 172 | try: 173 | 174 | response_2 = lambda_client.invoke( 175 | FunctionName = JOB_TRANSCRIPTOR_LAMBDA, 176 | InvocationType = 'Event' ,#'RequestResponse', 177 | Payload = json.dumps({ 178 | 'whats_message': whats_message, 179 | 'whats_token': whats_token, 180 | 'phone': phone, 181 | 'phone_id': phone_id, 182 | 'messages_id': messages_id 183 | 184 | }) 185 | ) 186 | 187 | print(f'\nRespuesta:{response_2}') 188 | 189 | return response_2 190 | 191 | except ClientError as e: 192 | err = e.response 193 | error = err 194 | print(err.get("Error", {}).get("Code")) 195 | return f"Un error invocando {JOB_TRANSCRIPTOR_LAMBDA}" -------------------------------------------------------------------------------- /04-whatsapp-app/lambdas/code/transcriber_done/lambda_function.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import json 3 | import os 4 | import requests 5 | from boto3.dynamodb.conditions import Key 6 | from utils import (normalize_phone,get_config,whats_reply) 7 | from botocore.exceptions import ClientError 8 | 9 | lambda_client = boto3.client('lambda') 10 | 11 | from db_utils import query_gd, query 12 | 13 | from file_utils import download_file 14 | 15 | table_name_active_connections = os.environ.get('whatsapp_MetaData') 16 | 17 | key_name_active_connections = os.environ.get('ENV_KEY_NAME') 18 | Index_Name = os.environ.get('ENV_INDEX_NAME') 19 | whatsapp_out_lambda = os.environ.get('WHATSAPP_OUT') 20 | ENV_AGENT_BEDROCK = os.environ['ENV_AGENT_BEDROCK'] 21 | 22 | client_s3 = boto3.client('s3') 23 | dynamodb_resource=boto3.resource('dynamodb') 24 | table = dynamodb_resource.Table(table_name_active_connections) 25 | 26 | base_path="/tmp/" 27 | 28 | def process_text(whats_message, whats_token,phone,phone_id,messages_id): 29 | 30 | print("text", whats_message, whats_token) 31 | ENV_AGENT_BEDROCK = os.environ['ENV_AGENT_BEDROCK'] 32 | 33 | try: 34 | 35 | response_3 = lambda_client.invoke( 36 | FunctionName = ENV_AGENT_BEDROCK, 37 | InvocationType = 'Event' ,#'RequestResponse', 38 | Payload = json.dumps({ 39 | 'whats_message': whats_message, 40 | 'whats_token': whats_token, 41 | 'phone': phone, 42 | 'phone_id': phone_id, 43 | 'messages_id': messages_id 44 | 45 | }) 46 | ) 47 | 48 | print(f'\nRespuesta:{response_3}') 49 | 50 | return response_3 51 | 52 | except ClientError as e: 53 | err = e.response 54 | error = err 55 | print(err.get("Error", {}).get("Code")) 56 | return f"Un error invocando {ENV_AGENT_BEDROCK}" 57 | 58 | 59 | def lambda_handler(event, context): 60 | 61 | print (event) 62 | 63 | for record in event['Records']: 64 | print("Event: ",event['Records']) 65 | record = event['Records'][0] 66 | 67 | s3bucket = record['s3']['bucket']['name'] 68 | s3object = record['s3']['object']['key'] 69 | filename = s3object.split("/")[-1] 70 | keyvalue = s3object.split("/")[-2] 71 | 72 | key = s3object.split("/")[1]+"/" 73 | print("s3object: ",s3object) 74 | print("filename: ",filename) 75 | print("key: ",key) 76 | 77 | try: 78 | 79 | if os.path.splitext(filename)[1] != ".temp": 80 | 81 | download_file(base_path,s3bucket, s3object, filename) 82 | value = filename.split("_")[-1].replace(".txt","").strip().replace(" ","") 83 | print(value) 84 | 85 | with open(base_path+filename) as f: 86 | message = f.readlines() 87 | 88 | messages_id = query_gd("jobName",table,value,Index_Name)[key_name_active_connections] 89 | whatsapp_data = query(key_name_active_connections,table,messages_id) 90 | message_json = json.loads(message[0]) 91 | print(message_json) 92 | text = message_json["results"]['transcripts'][0]['transcript'] 93 | phone = '+' + str(whatsapp_data['changes'][0]["value"]["messages"][0]["from"]) 94 | phone_number = str(whatsapp_data['changes'][0]["value"]["messages"][0]["from"]) 95 | whats_token = whatsapp_data['whats_token'] 96 | phone_id = whatsapp_data['changes'][0]["value"]["metadata"]["phone_number_id"] 97 | 98 | process_text(text, whats_token,phone,phone_id,messages_id) 99 | 100 | else: 101 | print("No se procesa el archivo") 102 | return True 103 | 104 | except ClientError as e: 105 | err = e.response 106 | error = err 107 | print(err.get("Error", {}).get("Code")) 108 | return f" error {filename}" -------------------------------------------------------------------------------- /04-whatsapp-app/lambdas/code/whatsapp_in/lambda_function.py: -------------------------------------------------------------------------------- 1 | 2 | #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 | ##++++++ Amazon Lambda Function for processing WhatsApp incoming messages +++++ 4 | ## Updated to Whatsapp API v14 5 | #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 | 7 | import json 8 | import os 9 | import boto3 10 | from botocore.exceptions import ClientError 11 | import time 12 | 13 | from utils import ( get_config,build_response,validate_healthcheck) 14 | 15 | table_name_active_connections = os.environ.get('whatsapp_MetaData') 16 | #whatsapp_MetaData_follow = os.environ.get('whatsapp_MetaData_follow') 17 | key_name_active_connections = "messages_id" 18 | 19 | dynamodb_resource=boto3.resource('dynamodb') 20 | table = dynamodb_resource.Table(table_name_active_connections) 21 | 22 | valid_display_phone_number = os.environ.get('DISPLAY_PHONE_NUMBER') 23 | 24 | CONFIG_PARAMETER= os.environ['CONFIG_PARAMETER'] 25 | 26 | def batch_put_items(client, table_name, item_arrays): 27 | table = client.Table(table_name) 28 | with table.batch_writer() as batch: 29 | for itm in item_arrays: 30 | res = batch.put_item(Item=itm) 31 | print(res) 32 | 33 | 34 | def lambda_handler(event, context): 35 | print (event) 36 | connect_config=json.loads(get_config(CONFIG_PARAMETER)) 37 | 38 | if event['httpMethod'] == 'GET': 39 | return build_response(200, 40 | validate_healthcheck(event, connect_config['WHATS_VERIFICATION_TOKEN'])) 41 | 42 | if event.get("body") == None: build_response(200,"bye bye") 43 | 44 | body = json.loads(event['body']) 45 | WHATS_TOKEN = 'Bearer ' + connect_config['WHATS_TOKEN'] 46 | 47 | ##WhatsApp specific iterations. 48 | for entry in body['entry']: 49 | print("Iterating entry") 50 | print(entry) 51 | display_phone_number=entry["changes"][0]["value"]["metadata"]["display_phone_number"] 52 | timestamp = int(entry["changes"][0]["value"]["messages"][0]["timestamp"]) 53 | now = int(time.time()) 54 | diferencia = now - timestamp 55 | if diferencia > 300: #session time in seg 56 | print("old message") 57 | break 58 | 59 | if display_phone_number == valid_display_phone_number: 60 | try: 61 | entry[key_name_active_connections]=entry["changes"][0]["value"]["messages"][0]["id"] 62 | entry["whats_token"] = WHATS_TOKEN 63 | print("key_name_active_connections",entry[key_name_active_connections]) 64 | batch_put_items(dynamodb_resource, table_name_active_connections, [entry]) 65 | #batch_put_items(dynamodb_resource, whatsapp_MetaData_follow, [entry]) 66 | except: 67 | print("No messages") 68 | else: 69 | print("No valid diplay phone number: ", display_phone_number) 70 | 71 | 72 | return build_response(200,"OK") 73 | -------------------------------------------------------------------------------- /04-whatsapp-app/lambdas/code/whatsapp_out/lambda_function.py: -------------------------------------------------------------------------------- 1 | 2 | import boto3 3 | import json 4 | import os 5 | import requests 6 | from boto3.dynamodb.conditions import Key 7 | from utils import ( normalize_phone,) 8 | 9 | base_path="/tmp/" 10 | 11 | def whats_out(phone, whats_token, phone_id, message, in_reply_to): 12 | 13 | # https://developers.facebook.com/docs/whatsapp/cloud-api/reference/messages#reply-to-message 14 | URL = 'https://graph.facebook.com/v15.0/'+phone_id+'/messages' 15 | headers = {'Authorization': whats_token, 'Content-Type': 'application/json'} 16 | data = { 17 | "messaging_product": "whatsapp", 18 | "to": normalize_phone(phone), 19 | "context": json.dumps({ "message_id": in_reply_to}), 20 | "type": "text", 21 | "text": json.dumps({ "preview_url": False, "body": message}) 22 | } 23 | 24 | print("Sending") 25 | print(data) 26 | response = requests.post(URL, headers=headers, data=data) 27 | responsejson = response.json() 28 | print("Responses: "+ str(responsejson) 29 | ) 30 | 31 | def lambda_handler(event, context): 32 | 33 | print (event) 34 | 35 | phone = event['phone'] 36 | whats_token = event['whats_token'] 37 | phone_id = event['phone_id'] 38 | message = event['message'] 39 | in_reply_to = event['in_reply_to'] 40 | 41 | try: 42 | whats_out(phone, whats_token, phone_id, message, in_reply_to) 43 | return True 44 | 45 | except Exception as error: 46 | print('FAILED!', error) 47 | return True -------------------------------------------------------------------------------- /04-whatsapp-app/lambdas/project_lambdas.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from aws_cdk import ( 4 | Duration, 5 | aws_lambda, 6 | aws_ssm as ssm, 7 | Stack, 8 | 9 | ) 10 | 11 | from constructs import Construct 12 | 13 | 14 | LAMBDA_TIMEOUT= 900 15 | 16 | BASE_LAMBDA_CONFIG = dict ( 17 | timeout=Duration.seconds(LAMBDA_TIMEOUT), 18 | memory_size=256, 19 | tracing= aws_lambda.Tracing.ACTIVE) 20 | 21 | COMMON_LAMBDA_CONF = dict (runtime=aws_lambda.Runtime.PYTHON_3_11, **BASE_LAMBDA_CONFIG) 22 | 23 | from layers import Layers 24 | 25 | 26 | class Lambdas(Construct): 27 | 28 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 29 | super().__init__(scope, construct_id, **kwargs) 30 | 31 | Lay = Layers(self, 'Lay') 32 | 33 | 34 | 35 | self.whatsapp_in = aws_lambda.Function( 36 | self, "whatsapp_in", handler="lambda_function.lambda_handler", 37 | description ="process WhatsApp incoming messages" , 38 | code=aws_lambda.Code.from_asset("./lambdas/code/whatsapp_in"), 39 | layers= [Lay.bs4_requests, Lay.common],**COMMON_LAMBDA_CONF) 40 | 41 | self.whatsapp_out = aws_lambda.Function( 42 | self, "whatsapp_out", handler="lambda_function.lambda_handler", 43 | description ="Send WhatsApp message" , 44 | code=aws_lambda.Code.from_asset("./lambdas/code/whatsapp_out"), 45 | layers= [Lay.bs4_requests, Lay.common],**COMMON_LAMBDA_CONF) 46 | 47 | self.audio_job_transcriptor = aws_lambda.Function( 48 | self, "audio_job_transcriptor", 49 | description ="Start Transcribe Job audio to text WhatsApp incoming messages" , 50 | handler="lambda_function.lambda_handler", 51 | code=aws_lambda.Code.from_asset("./lambdas/code/audio_job_transcriptor"), 52 | layers= [Lay.bs4_requests, Lay.common],**COMMON_LAMBDA_CONF) 53 | 54 | self.transcriber_done = aws_lambda.Function( 55 | self, "transcriber_done", 56 | description ="Read the text from transcriber job" , 57 | handler="lambda_function.lambda_handler", 58 | code=aws_lambda.Code.from_asset("./lambdas/code/transcriber_done"), 59 | layers= [Lay.bs4_requests, Lay.common],**COMMON_LAMBDA_CONF) 60 | 61 | self.process_stream = aws_lambda.Function( 62 | self, "process_stream", 63 | description ="Process Stream Lambda" , 64 | handler="lambda_function.lambda_handler", 65 | code=aws_lambda.Code.from_asset("./lambdas/code/process_stream"), 66 | layers= [Lay.bs4_requests, Lay.common],**COMMON_LAMBDA_CONF) 67 | 68 | 69 | self.agent_bedrock = aws_lambda.Function( 70 | self, "agent_bedrock", 71 | description ="invoke agent for amazon bedrock" , 72 | handler="lambda_function.lambda_handler", 73 | code=aws_lambda.Code.from_asset("./lambdas/code/agent_bedrock"), 74 | layers= [Lay.common], 75 | **COMMON_LAMBDA_CONF) 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /04-whatsapp-app/layers/__init__.py: -------------------------------------------------------------------------------- 1 | from layers.project_layers import Layers -------------------------------------------------------------------------------- /04-whatsapp-app/layers/aiofile-amazon-transcribe.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/04-whatsapp-app/layers/aiofile-amazon-transcribe.zip -------------------------------------------------------------------------------- /04-whatsapp-app/layers/bs4_requests.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/04-whatsapp-app/layers/bs4_requests.zip -------------------------------------------------------------------------------- /04-whatsapp-app/layers/common/python/agent_utils.py: -------------------------------------------------------------------------------- 1 | 2 | from langchain import LLMMathChain 3 | from langchain.llms.bedrock import Bedrock 4 | from langchain.memory import ConversationBufferMemory 5 | from langchain.memory.chat_message_histories import DynamoDBChatMessageHistory 6 | from langchain.agents import load_tools, initialize_agent, AgentType,Tool 7 | 8 | 9 | def match_function(model_id,bedrock_client): 10 | math_chain_llm = Bedrock(model_id=model_id,model_kwargs={"temperature":0,"stop_sequences" : ["```output"]},client=bedrock_client) 11 | llm_math_chain = LLMMathChain(llm=math_chain_llm, verbose=True) 12 | 13 | llm_math_chain.llm_chain.prompt.template = """Human: Given a question with a math problem, provide only a single line mathematical expression that solves the problem in the following format. Don't solve the expression only create a parsable expression. 14 | ```text 15 | ${{single line mathematical expression that solves the problem}} 16 | ``` 17 | 18 | Assistant: 19 | Here is an example response with a single line mathematical expression for solving a math problem: 20 | ```text 21 | 37593**(1/5) 22 | ``` 23 | 24 | Human: {question} 25 | Assistant:""" 26 | return Tool.from_function( 27 | func=llm_math_chain.run, 28 | name="Calculator", 29 | description="Useful for when you need to answer questions about math.", 30 | ) 31 | 32 | def memory_dynamodb(id,table_name_session): 33 | message_history = DynamoDBChatMessageHistory(table_name=table_name_session, session_id=id) 34 | memory = ConversationBufferMemory( 35 | memory_key="chat_history", chat_memory=message_history, return_messages=True,ai_prefix="A",human_prefix="H" 36 | ) 37 | return memory 38 | 39 | def langchain_agent(memory,tools,llm): 40 | zero_shot_agent=initialize_agent( 41 | llm=llm, 42 | agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, 43 | tools=tools, 44 | #verbose=True, 45 | max_iteration=1, 46 | #return_intermediate_steps=True, 47 | #handle_parsing_errors=True, 48 | memory=memory 49 | ) 50 | return zero_shot_agent -------------------------------------------------------------------------------- /04-whatsapp-app/layers/common/python/db_utils.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import requests 3 | from boto3.dynamodb.conditions import Key 4 | 5 | 6 | def query_gd(key,table,keyvalue,Index_Name): 7 | resp = table.query( 8 | # Add the name of the index you want to use in your query. 9 | IndexName="jobnameindex", 10 | KeyConditionExpression=Key(key).eq(keyvalue), 11 | ) 12 | print(resp) 13 | return resp['Items'][0] 14 | 15 | def query(key,table,keyvalue): 16 | response = table.query( 17 | KeyConditionExpression=Key(key).eq(keyvalue) 18 | ) 19 | print(response) 20 | return response['Items'][0] 21 | 22 | def update_item(value,end,duration,table): 23 | try: 24 | response = table.update_item( 25 | Key={ 26 | "messages_id" : value 27 | }, 28 | UpdateExpression="set end=:newState, duration=:newState1", 29 | ExpressionAttributeValues={ 30 | ':newState': end, 31 | ':newState1': duration, 32 | }, 33 | ReturnValues="UPDATED_NEW") 34 | print (response) 35 | except Exception as e: 36 | print (e) 37 | else: 38 | return response 39 | 40 | def save_item_ddb(table,item): 41 | response = table.put_item(Item=item) 42 | return response 43 | 44 | def update_items_out(table,value,response_out,end_response): 45 | try: 46 | response = table.update_item( 47 | Key={ 48 | "messages_id" : value 49 | }, 50 | UpdateExpression="set response_out=:item1, end_response=:item2", 51 | ExpressionAttributeValues={ 52 | ':item1': response_out, 53 | ':item2': end_response, 54 | }, 55 | ReturnValues="UPDATED_NEW") 56 | print (response) 57 | except Exception as e: 58 | print (e) 59 | else: 60 | return response 61 | 62 | def update_item_session(table_name_session,value,session_time): 63 | try: 64 | response = table_name_session.update_item( 65 | Key={ 66 | "phone_number" : value 67 | }, 68 | UpdateExpression="set session_time=:item1", 69 | ExpressionAttributeValues={ 70 | ':item1': session_time 71 | }, 72 | ReturnValues="UPDATED_NEW") 73 | print (response) 74 | except Exception as e: 75 | print (e) 76 | else: 77 | return response 78 | 79 | -------------------------------------------------------------------------------- /04-whatsapp-app/layers/common/python/file_utils.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import requests 3 | 4 | s3_resource = boto3.resource('s3') 5 | 6 | client_s3 = boto3.client('s3') 7 | 8 | def download_file(base_path,bucket, key, filename): 9 | print("Download file from s3://{}{}".format(bucket,key)) 10 | with open(base_path+filename, "wb") as data: 11 | client_s3.download_fileobj(bucket, key, data) 12 | print("Download file from s3://{}{}".format(bucket,key)) 13 | return True 14 | 15 | def upload_data_to_s3(bytes_data,bucket_name, s3_key): 16 | obj = s3_resource.Object(bucket_name, s3_key) 17 | obj.put(ACL='private', Body=bytes_data) 18 | s3_url = f"https://{bucket_name}.s3.amazonaws.com/{s3_key}" 19 | return s3_url 20 | 21 | def download_file_from_url(url): 22 | response = requests.get(url) 23 | if response.status_code == 200: 24 | return response.content 25 | else: 26 | return None 27 | 28 | def get_media_url(mediaId,whatsToken): 29 | 30 | URL = 'https://graph.facebook.com/v17.0/'+mediaId 31 | headers = {'Authorization': whatsToken} 32 | print("Requesting") 33 | response = requests.get(URL, headers=headers) 34 | responsejson = response.json() 35 | if('url' in responsejson): 36 | print("Responses: "+ str(responsejson)) 37 | return responsejson['url'] 38 | else: 39 | print("No URL returned") 40 | return None 41 | 42 | def get_whats_media(url,whatsToken): 43 | headers = {'Authorization': whatsToken} 44 | response = requests.get(url,headers=headers) 45 | if response.status_code == 200: 46 | return response.content 47 | else: 48 | return None 49 | 50 | def put_file(base_path,filename, bucket, key): 51 | with open(base_path+filename, "rb") as data: 52 | client_s3.upload_fileobj(data,bucket, key+filename) 53 | print("Put file in s3://{}{}{}".format(bucket,key,filename)) 54 | 55 | 56 | -------------------------------------------------------------------------------- /04-whatsapp-app/layers/common/python/utils.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import json 3 | from botocore.exceptions import ClientError 4 | secrets_client = boto3.client(service_name='secretsmanager') 5 | 6 | lambda_client = boto3.client('lambda') 7 | 8 | def get_config(secret_name): 9 | # Create a Secrets Manager client 10 | try: 11 | get_secret_value_response = secrets_client.get_secret_value( 12 | SecretId=secret_name 13 | ) 14 | except ClientError as e: 15 | if e.response['Error']['Code'] == 'DecryptionFailureException': 16 | # Secrets Manager can't decrypt the protected secret text using the provided KMS key. 17 | # Deal with the exception here, and/or rethrow at your discretion. 18 | raise e 19 | elif e.response['Error']['Code'] == 'InternalServiceErrorException': 20 | # An error occurred on the server side. 21 | # Deal with the exception here, and/or rethrow at your discretion. 22 | raise e 23 | elif e.response['Error']['Code'] == 'InvalidParameterException': 24 | # You provided an invalid value for a parameter. 25 | # Deal with the exception here, and/or rethrow at your discretion. 26 | raise e 27 | elif e.response['Error']['Code'] == 'InvalidRequestException': 28 | # You provided a parameter value that is not valid for the current state of the resource. 29 | # Deal with the exception here, and/or rethrow at your discretion. 30 | raise e 31 | elif e.response['Error']['Code'] == 'ResourceNotFoundException': 32 | # We can't find the resource that you asked for. 33 | # Deal with the exception here, and/or rethrow at your discretion. 34 | raise e 35 | else: 36 | # Decrypts secret using the associated KMS CMK. 37 | # Depending on whether the secret is a string or binary, one of these fields will be populated. 38 | if 'SecretString' in get_secret_value_response: 39 | secret = get_secret_value_response['SecretString'] 40 | else: 41 | secret = None 42 | return secret 43 | 44 | def normalize_phone(phone): 45 | ### Country specific changes required on phone numbers 46 | 47 | ### Mexico specific, remove 1 after 52 48 | if(phone[0:3]=='+52' and phone[3] == '1'): 49 | normalized = phone[0:3] + phone[4:] 50 | elif(phone[0:2]=='52' and phone[2] == '1'): 51 | normalized = phone[0:2] + phone[3:] 52 | else: 53 | normalized = phone 54 | return normalized 55 | ### End Mexico specific 56 | 57 | def get_file_category(mimeType): 58 | ## Possible {AUDIO, CONTACTS, DOCUMENT, IMAGE, TEXT, TEMPLATE, VIDEO, STICKER, LOCATION, INTERACTIVE, REACTION} 59 | if('application' in mimeType): return 'document' 60 | elif('image' in mimeType): return 'image' 61 | elif('audio' in mimeType): return 'audio' 62 | elif('video' in mimeType): return 'video' 63 | 64 | 65 | def build_response(status_code, json_content): 66 | return { 67 | 'statusCode': status_code, 68 | "headers": { 69 | "Content-Type": "text/html;charset=UTF-8", 70 | "charset": "UTF-8", 71 | "Access-Control-Allow-Origin": "*" 72 | }, 73 | 'body': json_content 74 | } 75 | 76 | def validate_healthcheck(event, WHATS_VERIFICATION_TOKEN ): 77 | if('queryStringParameters' in event and 'hub.challenge' in event['queryStringParameters']): 78 | print(event['queryStringParameters']) 79 | print("Token challenge") 80 | if(event['queryStringParameters']['hub.verify_token'] == WHATS_VERIFICATION_TOKEN): 81 | print("Token verified") 82 | print(event['queryStringParameters']['hub.challenge']) 83 | response = event['queryStringParameters']['hub.challenge'] 84 | else: 85 | response = '' 86 | else: 87 | print("Not challenge related") 88 | response = ' No key, no fun!' 89 | return response 90 | 91 | def whats_reply(whatsapp_out_lambda,phone, whats_token, phone_id, message, in_reply_to): 92 | 93 | print("WHATSAPP_OUT", in_reply_to, whats_token) 94 | 95 | try: 96 | 97 | response_2 = lambda_client.invoke( 98 | FunctionName = whatsapp_out_lambda, 99 | InvocationType = 'Event' ,#'RequestResponse', 100 | Payload = json.dumps({ 101 | 'phone': phone, 102 | 'whats_token': whats_token, 103 | 'phone_id': phone_id, 104 | 'message': message, 105 | 'in_reply_to': in_reply_to 106 | 107 | }) 108 | ) 109 | 110 | print(f'\nRespuesta:{response_2}') 111 | 112 | return response_2 113 | except ClientError as e: 114 | err = e.response 115 | error = err 116 | print(err.get("Error", {}).get("Code")) 117 | return f"Un error invocando {whatsapp_out_lambda}" -------------------------------------------------------------------------------- /04-whatsapp-app/layers/project_layers.py: -------------------------------------------------------------------------------- 1 | import json 2 | from constructs import Construct 3 | 4 | from aws_cdk import ( 5 | aws_lambda 6 | 7 | ) 8 | 9 | 10 | class Layers(Construct): 11 | 12 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 13 | super().__init__(scope, construct_id, **kwargs) 14 | 15 | #layer con beautiful soup y requests 16 | bs4_requests = aws_lambda.LayerVersion( 17 | self, "Bs4Requests", code=aws_lambda.Code.from_asset("./layers/bs4_requests.zip"), 18 | compatible_runtimes = [ 19 | aws_lambda.Runtime.PYTHON_3_8, 20 | aws_lambda.Runtime.PYTHON_3_9, 21 | aws_lambda.Runtime.PYTHON_3_10, 22 | aws_lambda.Runtime.PYTHON_3_11], 23 | description = 'BeautifulSoup y Requests') 24 | 25 | self.bs4_requests = bs4_requests 26 | 27 | aiofile_transcribe = aws_lambda.LayerVersion( 28 | self, "aiofile-transcribe", code=aws_lambda.Code.from_asset("./layers/aiofile-amazon-transcribe.zip"), 29 | compatible_runtimes = [ 30 | aws_lambda.Runtime.PYTHON_3_8, 31 | aws_lambda.Runtime.PYTHON_3_9, 32 | aws_lambda.Runtime.PYTHON_3_10, 33 | aws_lambda.Runtime.PYTHON_3_11], 34 | description = 'aiofile y amazon-transcribe', layer_version_name = "aiofile-transcribe-streamig" 35 | ) 36 | 37 | self.aiofile_transcribe = aiofile_transcribe 38 | 39 | common = aws_lambda.LayerVersion( 40 | self, "common-layer", code=aws_lambda.Code.from_asset("./layers/common/"), 41 | compatible_runtimes = [ 42 | aws_lambda.Runtime.PYTHON_3_8, 43 | aws_lambda.Runtime.PYTHON_3_9, 44 | aws_lambda.Runtime.PYTHON_3_10, 45 | aws_lambda.Runtime.PYTHON_3_11], 46 | description = 'librerias adicionales', layer_version_name = "librerias-adicionales" 47 | ) 48 | 49 | self.common = common 50 | 51 | -------------------------------------------------------------------------------- /04-whatsapp-app/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest==6.2.5 2 | -------------------------------------------------------------------------------- /04-whatsapp-app/requirements.txt: -------------------------------------------------------------------------------- 1 | aws-cdk-lib==2.146.0 2 | constructs>=10.0.0,<11.0.0 3 | boto3 -------------------------------------------------------------------------------- /04-whatsapp-app/s3_cloudfront/__init__.py: -------------------------------------------------------------------------------- 1 | from s3_cloudfront.s3_cloudfront_website import S3Deploy 2 | -------------------------------------------------------------------------------- /04-whatsapp-app/s3_cloudfront/s3_cloudfront/__init__.py: -------------------------------------------------------------------------------- 1 | from s3_cloudfront.s3_cloudfront_website import S3Deploy 2 | -------------------------------------------------------------------------------- /04-whatsapp-app/s3_cloudfront/s3_cloudfront/s3_cloudfront_website.py: -------------------------------------------------------------------------------- 1 | from constructs import Construct 2 | from aws_cdk import ( 3 | aws_s3_deployment as s3deploy, 4 | aws_s3 as s3, RemovalPolicy) 5 | 6 | 7 | class S3Deploy(Construct): 8 | def __init__(self, scope: Construct, id: str, files_location, dest_prefix,**kwargs) -> None: 9 | super().__init__(scope, id, **kwargs) 10 | 11 | self.bucket = s3.Bucket(self, "Bucket",access_control=s3.BucketAccessControl.PRIVATE, removal_policy=RemovalPolicy.DESTROY) 12 | 13 | 14 | self.s3deploy = s3deploy.BucketDeployment(self, "Deployment", 15 | sources=[s3deploy.Source.asset(files_location)], 16 | destination_bucket = self.bucket, 17 | retain_on_delete=False, 18 | destination_key_prefix=dest_prefix 19 | ) 20 | 21 | 22 | def deploy(self, id, files_loc, prefix): 23 | deployment = s3deploy.BucketDeployment(self, id, 24 | sources=[s3deploy.Source.asset(files_loc)], 25 | destination_bucket = self.bucket, 26 | retain_on_delete=False, 27 | destination_key_prefix=prefix 28 | ) 29 | return deployment 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /04-whatsapp-app/s3_cloudfront/s3_cloudfront_website.py: -------------------------------------------------------------------------------- 1 | from constructs import Construct 2 | from aws_cdk import ( 3 | aws_s3_deployment as s3deploy, 4 | aws_s3 as s3, RemovalPolicy) 5 | 6 | 7 | class S3Deploy(Construct): 8 | def __init__(self, scope: Construct, id: str, files_location, dest_prefix,**kwargs) -> None: 9 | super().__init__(scope, id, **kwargs) 10 | 11 | self.bucket = s3.Bucket(self, "Bucket",access_control=s3.BucketAccessControl.PRIVATE, removal_policy=RemovalPolicy.DESTROY) 12 | 13 | 14 | self.s3deploy = s3deploy.BucketDeployment(self, "Deployment", 15 | sources=[s3deploy.Source.asset(files_location)], 16 | destination_bucket = self.bucket, 17 | retain_on_delete=False, 18 | destination_key_prefix=dest_prefix 19 | ) 20 | 21 | 22 | def deploy(self, id, files_loc, prefix): 23 | deployment = s3deploy.BucketDeployment(self, id, 24 | sources=[s3deploy.Source.asset(files_loc)], 25 | destination_bucket = self.bucket, 26 | retain_on_delete=False, 27 | destination_key_prefix=prefix 28 | ) 29 | return deployment 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /04-whatsapp-app/source.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | rem The sole purpose of this script is to make the command 4 | rem 5 | rem source .venv/bin/activate 6 | rem 7 | rem (which activates a Python virtualenv on Linux or Mac OS X) work on Windows. 8 | rem On Windows, this command just runs this batch file (the argument is ignored). 9 | rem 10 | rem Now we don't need to document a Windows command for activating a virtualenv. 11 | 12 | echo Executing .venv\Scripts\activate.bat for you 13 | .venv\Scripts\activate.bat 14 | -------------------------------------------------------------------------------- /04-whatsapp-app/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/04-whatsapp-app/tests/__init__.py -------------------------------------------------------------------------------- /04-whatsapp-app/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/04-whatsapp-app/tests/unit/__init__.py -------------------------------------------------------------------------------- /04-whatsapp-app/tests/unit/test_whatsapp_app_stack.py: -------------------------------------------------------------------------------- 1 | import aws_cdk as core 2 | import aws_cdk.assertions as assertions 3 | 4 | from whatsapp_app.whatsapp_app_stack import WhatsappAppStack 5 | 6 | # example tests. To run these tests, uncomment this file along with the example 7 | # resource in whatsapp_app/whatsapp_app_stack.py 8 | def test_sqs_queue_created(): 9 | app = core.App() 10 | stack = WhatsappAppStack(app, "whatsapp-app") 11 | template = assertions.Template.from_stack(stack) 12 | 13 | # template.has_resource_properties("AWS::SQS::Queue", { 14 | # "VisibilityTimeout": 300 15 | # }) 16 | -------------------------------------------------------------------------------- /04-whatsapp-app/text-to-whatsapp/test.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/04-whatsapp-app/text-to-whatsapp/test.txt -------------------------------------------------------------------------------- /04-whatsapp-app/whatsapp_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/04-whatsapp-app/whatsapp_app/__init__.py -------------------------------------------------------------------------------- /04-whatsapp-app/whatsapp_app/whatsapp_app_stack.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | # Duration, 3 | Stack,SecretValue, 4 | # aws_sqs as sqs, 5 | RemovalPolicy, 6 | aws_dynamodb as ddb, 7 | aws_secretsmanager as secretsmanager, 8 | aws_iam as iam, 9 | aws_s3_notifications, 10 | aws_s3 as s3, 11 | aws_lambda, 12 | aws_lambda_event_sources, 13 | aws_ssm as ssm, 14 | ) 15 | from constructs import Construct 16 | from lambdas import Lambdas 17 | from apis import WebhookApi 18 | from databases import Tables 19 | from s3_cloudfront import S3Deploy 20 | 21 | from get_param import get_string_param 22 | 23 | agent_id = get_string_param("/agentvalue/agent_id") 24 | alias_id = get_string_param("/agentvalue/alias_id") 25 | 26 | class WhatsappAppStack(Stack): 27 | 28 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 29 | super().__init__(scope, construct_id, **kwargs) 30 | 31 | REMOVAL_POLICY = RemovalPolicy.DESTROY 32 | TABLE_CONFIG = dict (removal_policy=REMOVAL_POLICY, billing_mode= ddb.BillingMode.PAY_PER_REQUEST) 33 | AudioKeyName = "audio-from-whatsapp" 34 | TextBucketName = "text-to-whatsapp" 35 | 36 | DISPLAY_PHONE_NUMBER = 'YOU-NUMBER' 37 | 38 | stk = Stack.of(self) 39 | _account = stk.account 40 | _region = stk.region 41 | _stack_name = stk.stack_name 42 | 43 | #Whatsapp Secrets Values 44 | secrets = secretsmanager.Secret(self, "Secrets", secret_object_value = { 45 | 'WHATS_VERIFICATION_TOKEN': SecretValue.unsafe_plain_text('WHATS_VERIFICATION_TOKEN'), 46 | 'WHATS_PHONE_ID':SecretValue.unsafe_plain_text('WHATS_PHONE_ID'), 47 | 'WHATS_TOKEN': SecretValue.unsafe_plain_text('WHATS_TOKEN') 48 | }) 49 | 50 | Tbl = Tables(self, 'Tbl') 51 | 52 | Tbl.whatsapp_MetaData.add_global_secondary_index(index_name = 'jobnameindex', 53 | partition_key = ddb.Attribute(name="jobName",type=ddb.AttributeType.STRING), 54 | projection_type=ddb.ProjectionType.KEYS_ONLY) 55 | 56 | s3_deploy = S3Deploy(self, "-data", TextBucketName,TextBucketName) 57 | 58 | #Create Amazon Lambda Functions 59 | Fn = Lambdas(self,'Fn') 60 | 61 | #Create Amazon API Gateweay 62 | Api = WebhookApi(self, "API", lambdas=Fn) 63 | 64 | # Amazon Lambda Function whatsapp_in - Config 65 | Fn.whatsapp_in.add_environment(key='CONFIG_PARAMETER', value=secrets.secret_arn) 66 | secrets.grant_read(Fn.whatsapp_in) 67 | 68 | Fn.whatsapp_in.add_environment(key='REFRESH_SECRETS', value='false') 69 | Fn.whatsapp_in.add_environment(key='DISPLAY_PHONE_NUMBER', value= DISPLAY_PHONE_NUMBER) 70 | 71 | Tbl.whatsapp_MetaData.grant_full_access(Fn.whatsapp_in) 72 | 73 | Fn.whatsapp_in.add_environment(key='whatsapp_MetaData', value=Tbl.whatsapp_MetaData.table_name) 74 | 75 | Fn.process_stream.add_environment( key='ENV_AGENT_BEDROCK', value=Fn.agent_bedrock.function_name) 76 | Fn.process_stream.add_environment(key='JOB_TRANSCRIPTOR_LAMBDA', value=Fn.audio_job_transcriptor.function_name) 77 | Fn.process_stream.add_environment(key='whatsapp_MetaData', value=Tbl.whatsapp_MetaData.table_name) 78 | Fn.process_stream.add_environment( key='WHATSAPP_OUT', value=Fn.whatsapp_out.function_name ) 79 | 80 | Fn.process_stream.add_event_source( 81 | aws_lambda_event_sources.DynamoEventSource(table=Tbl.whatsapp_MetaData, 82 | starting_position=aws_lambda.StartingPosition.TRIM_HORIZON)) 83 | Tbl.whatsapp_MetaData.grant_full_access(Fn.process_stream) 84 | 85 | 86 | # Amazon Lambda Function whatsapp_out - Config 87 | 88 | Fn.whatsapp_out.add_environment(key='ENV_INDEX_NAME', value="jobnameindex") 89 | Fn.whatsapp_out.add_environment(key='ENV_KEY_NAME', value="messages_id") 90 | 91 | Fn.whatsapp_out.grant_invoke(Fn.agent_bedrock) 92 | 93 | # Amazon Lambda Function audio_job_transcriptor - Config 94 | 95 | Fn.audio_job_transcriptor.add_to_role_policy(iam.PolicyStatement( actions=["transcribe:*"], resources=['*'])) 96 | Fn.audio_job_transcriptor.add_environment(key='BucketName', value=s3_deploy.bucket.bucket_name) 97 | Fn.audio_job_transcriptor.add_environment(key='whatsapp_MetaData', value=Tbl.whatsapp_MetaData.table_name) 98 | Fn.audio_job_transcriptor.add_environment(key='AudioKeyName', value=AudioKeyName) 99 | Fn.audio_job_transcriptor.add_environment(key='TextBucketName', value=TextBucketName) 100 | Fn.audio_job_transcriptor.grant_invoke(Fn.process_stream) 101 | Fn.audio_job_transcriptor.add_to_role_policy(iam.PolicyStatement( actions=["dynamodb:*"], resources=[f"{Tbl.whatsapp_MetaData.table_arn}",f"{Tbl.whatsapp_MetaData.table_arn}/*"])) 102 | Fn.audio_job_transcriptor.add_environment(key='ENV_INDEX_NAME', value="jobnameindex") 103 | Fn.audio_job_transcriptor.add_environment(key='ENV_KEY_NAME', value="messages_id") 104 | 105 | s3_deploy.bucket.grant_read_write(Fn.audio_job_transcriptor) 106 | Tbl.whatsapp_MetaData.grant_full_access(Fn.audio_job_transcriptor) 107 | 108 | # Amazon Lambda Function audio_job_transcriptor done - Config 109 | 110 | s3_deploy.bucket.grant_read(Fn.transcriber_done) 111 | 112 | s3_deploy.bucket.add_event_notification(s3.EventType.OBJECT_CREATED, 113 | aws_s3_notifications.LambdaDestination(Fn.transcriber_done), 114 | s3.NotificationKeyFilter(prefix=TextBucketName+"/")) 115 | 116 | Fn.transcriber_done.add_environment( key='WHATSAPP_OUT', value=Fn.whatsapp_out.function_name ) 117 | Fn.transcriber_done.add_to_role_policy(iam.PolicyStatement( actions=["dynamodb:*"], resources=[f"{Tbl.whatsapp_MetaData.table_arn}",f"{Tbl.whatsapp_MetaData.table_arn}/*"])) 118 | Fn.transcriber_done.add_environment(key='ENV_INDEX_NAME', value="jobnameindex") 119 | Fn.transcriber_done.add_environment(key='ENV_KEY_NAME', value="messages_id") 120 | 121 | Fn.whatsapp_out.grant_invoke(Fn.transcriber_done) 122 | 123 | Tbl.whatsapp_MetaData.grant_full_access(Fn.transcriber_done) 124 | 125 | Fn.transcriber_done.add_environment(key='whatsapp_MetaData', value=Tbl.whatsapp_MetaData.table_name) 126 | 127 | Fn.agent_bedrock.grant_invoke(Fn.transcriber_done) 128 | Fn.agent_bedrock.add_environment(key='ENV_AGENT_ID', value=agent_id) 129 | Fn.agent_bedrock.add_environment(key='ENV_ALIAS_ID', value=alias_id) 130 | Fn.agent_bedrock.add_environment( key='WHATSAPP_OUT', value=Fn.whatsapp_out.function_name ) 131 | 132 | Fn.agent_bedrock.grant_invoke(Fn.process_stream) 133 | 134 | Fn.agent_bedrock.add_to_role_policy( 135 | iam.PolicyStatement( 136 | actions=["bedrock:InvokeAgent"], 137 | resources=[f"arn:aws:bedrock:{_region}:{_account}:*"] 138 | ) 139 | ) 140 | 141 | Fn.transcriber_done.add_environment( key='ENV_AGENT_BEDROCK', value=Fn.agent_bedrock.function_name) 142 | 143 | Fn.agent_bedrock.grant_invoke(Fn.transcriber_done) 144 | 145 | Tbl.whatsapp_MetaData.grant_full_access(Fn.agent_bedrock) 146 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /imagen/check_aurora.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/imagen/check_aurora.png -------------------------------------------------------------------------------- /imagen/diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/imagen/diagram.png -------------------------------------------------------------------------------- /imagen/diagram_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/imagen/diagram_1.jpg -------------------------------------------------------------------------------- /imagen/diagram_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/imagen/diagram_2.png -------------------------------------------------------------------------------- /imagen/part_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-on-aws/rag-postgresql-agent-bedrock/977f269327723b25a37e3158d0902c5d44f1056a/imagen/part_1.jpg --------------------------------------------------------------------------------