diff --git a/CHANGELOG.md b/CHANGELOG.md index de192b01..b915dfa5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - added `sagemaker-model-package-promote-pipeline` module. - added `sagemaker-hugging-face-endpoint` module - added `hf_import_models` template to import hugging face models +- added `qna-rag` module ### **Changed** diff --git a/README.md b/README.md index 44f4af79..c5b041fa 100644 --- a/README.md +++ b/README.md @@ -42,10 +42,11 @@ See deployment steps in the [Deployment Guide](DEPLOYMENT.md). ### FMOps Modules -| Type | Description | -|-----------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------| -| [SageMaker JumpStart Foundation Model Endpoint Module](modules/fmops/sagemaker-jumpstart-fm-endpoint/README.md) | Creates an endpoint for a SageMaker JumpStart Foundation Model. | -| [SageMaker Hugging Face Foundation Model Endpoint Module](modules/fmops/sagemaker-hugging-face-endpoint/README.md) | Creates an endpoint for a SageMaker Hugging Face Foundation Model. | +| Type | Description | +|--------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------| +| [SageMaker JumpStart Foundation Model Endpoint Module](modules/fmops/sagemaker-jumpstart-fm-endpoint/README.md) | Creates an endpoint for a SageMaker JumpStart Foundation Model. | +| [SageMaker Hugging Face Foundation Model Endpoint Module](modules/fmops/sagemaker-hugging-face-endpoint/README.md) | Creates an endpoint for a SageMaker Hugging Face Foundation Model. | +| [AppSync Knowledge Base Ingestion and Question and Answering RAG Module](modules/fmops/qna-rag/README.md) | Creates an Graphql endpoint for ingestion of data and and use ingested as knowledge base for a Question and Answering model using RAG. | ### MWAA Modules diff --git a/examples/manifests/deployment.yaml b/examples/manifests/deployment.yaml index 808345c2..b666b6eb 100644 --- a/examples/manifests/deployment.yaml +++ b/examples/manifests/deployment.yaml @@ -14,6 +14,7 @@ groups: path: manifests/sagemaker-model-package-group-modules.yaml - name: promote-models path: manifests/sagemaker-model-package-promote-pipeline-modules.yaml + targetAccountMappings: - alias: primary accountId: diff --git a/manifests/fmops-qna-rag/deployment.yaml b/manifests/fmops-qna-rag/deployment.yaml new file mode 100644 index 00000000..d278d771 --- /dev/null +++ b/manifests/fmops-qna-rag/deployment.yaml @@ -0,0 +1,20 @@ +name: mlops-qna-rag +toolchainRegion: us-east-1 +forceDependencyRedeploy: true +groups: + - name: networking + path: manifests/fmops-qna-rag/networking-modules.yaml + - name: storage + path: manifests/fmops-qna-rag/storage-modules.yaml + - name: qna-rag + path: manifests/fmops-qna-rag/qna-rag-modules.yaml + +targetAccountMappings: + - alias: primary + accountId: + valueFrom: + envVariable: PRIMARY_ACCOUNT + default: true + regionMappings: + - region: us-east-1 + default: true \ No newline at end of file diff --git a/manifests/fmops-qna-rag/networking-modules.yaml b/manifests/fmops-qna-rag/networking-modules.yaml new file mode 100644 index 00000000..1e58a365 --- /dev/null +++ b/manifests/fmops-qna-rag/networking-modules.yaml @@ -0,0 +1,6 @@ +name: networking +path: git::https://github.com/awslabs/idf-modules.git//modules/network/basic-cdk?ref=release/1.3.0&depth=1 +targetAccount: primary +parameters: + - name: internet-accessible + value: True \ No newline at end of file diff --git a/manifests/fmops-qna-rag/qna-rag-modules.yaml b/manifests/fmops-qna-rag/qna-rag-modules.yaml new file mode 100644 index 00000000..f2676ee9 --- /dev/null +++ b/manifests/fmops-qna-rag/qna-rag-modules.yaml @@ -0,0 +1,24 @@ +name: qna-rag +path: modules/fmops/qna-rag +parameters: + - name: cognito-pool-id + #Replace below value with valid congnito pool id + value: us-east-1_XXXXX + - name: os-domain-endpoint + valueFrom: + moduleMetadata: + group: storage + name: opensearch + key: OpenSearchDomainEndpoint + - name: os-security-group-id + valueFrom: + moduleMetadata: + group: storage + name: opensearch + key: OpenSearchSecurityGroupId + - name: vpc-id + valueFrom: + moduleMetadata: + group: networking + name: networking + key: VpcId \ No newline at end of file diff --git a/manifests/fmops-qna-rag/storage-modules.yaml b/manifests/fmops-qna-rag/storage-modules.yaml new file mode 100644 index 00000000..df17cbf7 --- /dev/null +++ b/manifests/fmops-qna-rag/storage-modules.yaml @@ -0,0 +1,22 @@ +--- +name: opensearch +path: git::https://github.com/awslabs/idf-modules.git//modules/storage/opensearch?ref=release/1.7.0&depth=1 +targetAccount: primary +targetRegion: us-east-1 +parameters: + - name: encryption-type + value: SSE + - name: retention-type + value: RETAIN + - name: vpc-id + valueFrom: + moduleMetadata: + group: networking + name: networking + key: VpcId + - name: private-subnet-ids + valueFrom: + moduleMetadata: + group: networking + name: networking + key: PrivateSubnetIds \ No newline at end of file diff --git a/manifests/qna-rag-modules.yaml b/manifests/qna-rag-modules.yaml new file mode 100644 index 00000000..f2676ee9 --- /dev/null +++ b/manifests/qna-rag-modules.yaml @@ -0,0 +1,24 @@ +name: qna-rag +path: modules/fmops/qna-rag +parameters: + - name: cognito-pool-id + #Replace below value with valid congnito pool id + value: us-east-1_XXXXX + - name: os-domain-endpoint + valueFrom: + moduleMetadata: + group: storage + name: opensearch + key: OpenSearchDomainEndpoint + - name: os-security-group-id + valueFrom: + moduleMetadata: + group: storage + name: opensearch + key: OpenSearchSecurityGroupId + - name: vpc-id + valueFrom: + moduleMetadata: + group: networking + name: networking + key: VpcId \ No newline at end of file diff --git a/manifests/storage-modules.yaml b/manifests/storage-modules.yaml index 150b1e74..eb166a98 100644 --- a/manifests/storage-modules.yaml +++ b/manifests/storage-modules.yaml @@ -51,3 +51,25 @@ parameters: value: 30 - name: removal-policy value: DESTROY +--- +name: opensearch +path: git::https://github.com/awslabs/idf-modules.git//modules/storage/opensearch?ref=release/1.7.0&depth=1 +targetAccount: primary +targetRegion: us-east-1 +parameters: + - name: encryption-type + value: SSE + - name: retention-type + value: RETAIN + - name: vpc-id + valueFrom: + moduleMetadata: + group: networking + name: networking + key: VpcId + - name: private-subnet-ids + valueFrom: + moduleMetadata: + group: networking + name: networking + key: PrivateSubnetIds \ No newline at end of file diff --git a/manifests/uber-deployment.yaml b/manifests/uber-deployment.yaml index d4f3a312..33b3cbdf 100644 --- a/manifests/uber-deployment.yaml +++ b/manifests/uber-deployment.yaml @@ -22,6 +22,8 @@ groups: path: manifests/mwaa-modules.yaml - name: mwaa-dags path: manifests/mwaa-dag-modules.yaml + - name: qna-rag + path: manifests/qna-rag-modules.yaml targetAccountMappings: - alias: primary accountId: diff --git a/modules/fmops/qna-rag/README.md b/modules/fmops/qna-rag/README.md new file mode 100644 index 00000000..c6c294b3 --- /dev/null +++ b/modules/fmops/qna-rag/README.md @@ -0,0 +1,261 @@ +# AppSync endpoint for Question and Answering using RAG + +## Description + +Deploys an AWS AppSync endpoint for ingestion of data and use it as knowledge base for a Question and Answering model using RAG + +The module uses [AWS Generative AI CDK Constructs](https://github.com/awslabs/generative-ai-cdk-constructs/tree/main). + +### Architecture +Knowledge Base Ingestion Architecture +![AWS Appsync Ingestion Endpoint Module Architecture](docs/_static/ingestion_architecture.png "AWS Appsync Ingestion Endpoint Module Architecture") + +Question and Answering using RAG Architecture +![AWS Appsync Question and Answering Endpoint Module Architecture](docs/_static/architecture.png "AWS Appsync Question and Answering RAG module Endpoint Module Architecture") + +## Inputs/Outputs + +### Input Parameters + +#### Required + +- `cognito-pool-id` - ID of the cognito user pool, used to secure GraphQl API +- `os-domain-endpoint` - Open Search doamin url used as knowledge base +- `os-security-group-id` - Security group of open search cluster +- `vpc-id` - VPC id + +#### Optional + +- `input-asset-bucket` - Input asset bucket that is used to store input documents + +### Module Metadata Outputs + +- `IngestionGraphqlApiId` - Ingestion Graphql API ID. +- `IngestionGraphqlArn` - Ingestion Graphql API ARN. +- `QnAGraphqlApiId` - Graphql API ID. +- `QnAGraphqlArn` - Graphql API ARN. +- `InputAssetBucket` - Input S3 bucket. +- `ProcessedInputBucket` - S3 bucket for storing processed output. + +## Examples + +Example manifest: + +```yaml +name: qna-rag +path: modules/fmops/qna-rag +parameters: + - name: cognito-pool-id + #Replace below value with valid congnito pool id + value: us-east-1_XXXXX + - name: os-domain-endpoint + valueFrom: + moduleMetadata: + group: storage + name: opensearch + key: OpenSearchDomainEndpoint + - name: os-security-group-id + valueFrom: + moduleMetadata: + group: storage + name: opensearch + key: OpenSearchSecurityGroupId + - name: vpc-id + valueFrom: + moduleMetadata: + group: networking + name: networking + key: VpcId + +``` +After deploying the Seedfarmer stack, Upload the file to be ingested into the input S3 bucket(If no input S3 bucket is provided in manifest, a bucket with name 'input-assets-bucket-dev-' will be created by the construct) + +The document summarization workflow can be invoked using GraphQL APIs. First invoke Subscription call followed by mutation call. + +The code below provides an example of a mutation call and associated subscription to trigger a pipeline call and get status notifications: + +Subscription call to get notifications about the ingestion process: + +``` +subscription MySubscription { + updateIngestionJobStatus(ingestionjobid: "123") { + files { + name + status + imageurl + } + } +} +_________________________________________________ +Expected response: + +{ + "data": { + "updateIngestionJobStatus": { + "files": [ + { + "name": "a.pdf", + "status": "succeed", + "imageurl":"s3presignedurl" + } + ] + } + } +} +``` +Where: +- ingestionjobid: id which can be used to filter subscriptions on client side + The subscription will display the status and name for each file +- files.status: status update of the ingestion for the file specified +- files.name: name of the file stored in the input S3 bucket + +Mutation call to trigger the ingestion process: + +``` +mutation MyMutation { + ingestDocuments(ingestioninput: { + embeddings_model: + { + provider: Bedrock, + modelId: "amazon.titan-embed-text-v1", + streaming: true + }, + files: [{status: "", name: "a.pdf"}], + ingestionjobid: "123", + ignore_existing: true}) { + files { + imageurl + status + } + ingestionjobid + } +} +_________________________________________________ +Expected response: + +{ + "data": { + "ingestDocuments": { + "ingestionjobid": null + } + } +} +``` +Where: +- files.status: this field will be used by the subscription to update the status of the ingestion for the file specified +- files.name: name of the file stored in the input S3 bucket +- ingestionjobid: id which can be used to filter subscriptions on client side +- embeddings_model: Based on type of modality (text or image ) the model provider , model id can be used. + + + +After ingesting the input files , the QA process can be invoked using GraphQL APIs. First invoke Subscription call followed by mutation call. + +The code below provides an example of a mutation call and associated subscription to trigger a question and get response notifications. The subscription call will wait for mutation requests to send the notifications. + +Subscription call to get notifications about the question answering process: + +``` +subscription MySubscription { + updateQAJobStatus(jobid: "123") { + sources + question + answer + jobstatus + } +} +____________________________________________________________________ +Expected response: + +{ + "data": { + "updateQAJobStatus": { + "sources": [ + "" + ], + "question": "", + "answer": "", + "jobstatus": "Succeed" + } + } +} +``` + +Where: + +- jobid: id which can be used to filter subscriptions on client side +- answer: response to the question from the large language model as a base64 encoded string +- sources: sources from the knowledge base used as context to answer the question +- jobstatus: status update of the question answering process for the file specified + +Mutation call to trigger the question: + +``` +mutation MyMutation { + postQuestion(filename: "", + embeddings_model: + { + modality: "Text", + modelId: "amazon.titan-embed-text-v1", + provider: Bedrock, + streaming: false + }, + filename:"" + jobid: "123", + jobstatus: "", + qa_model: + { + provider: Bedrock, + modality: "Text", + modelId: "anthropic.claude-v2:1", + streaming: false, + model_kwargs: "{\"temperature\":0.5,\"top_p\":0.9,\"max_tokens_to_sample\":250}" + }, + question:"", + responseGenerationMethod: RAG + , + retrieval:{ + max_docs:10 + }, + verbose:false + + ) { + jobid + question + verbose + filename + answer + jobstatus + responseGenerationMethod + } +} +____________________________________________________________________ +Expected response: + +{ + "data": { + "postQuestion": { + "jobid": null, + "question": null, + "verbose": null, + "filename": null, + "answer": null, + "jobstatus": null, + "responseGenerationMethod": null + } + } +} +``` + +Where: + +- jobid: id which can be used to filter subscriptions on client side +- jobstatus: this field will be used by the subscription to update the status of the question answering process for the file specified +- qa_model.modality/embeddings_model.modality: Applicable values Text or Image +- qa_model.modelId/embeddings_model.modelId: Model to process Q&A. example - anthropic.claude-v2:1,Claude-3-sonnet-20240229-v1:0 +- retrieval.max_docs: maximum number of documents (chunks) retrieved from the knowledge base if the Retrieveal Augmented Generation (RAG) approach is used +- question: question to ask as a base64 encoded string +- verbose: boolean indicating if the [LangChain chain call verbosity](https://python.langchain.com/docs/guides/debugging#chain-verbosetrue) should be enabled or not +- streaming: boolean indicating if the streaming capability of Bedrock is used. If set to true, tokens will be send back to the subscriber as they are generated. If set to false, the entire response will be sent back to the subscriber once generated. +- filename: optional. Name of the file stored in the input S3 bucket, in txt format. +- responseGenerationMethod: optional. Method used to generate the response. Can be either RAG or LONG_CONTEXT. If not provided, the default value is LONG_CONTEXT. diff --git a/modules/fmops/qna-rag/app.py b/modules/fmops/qna-rag/app.py new file mode 100644 index 00000000..cfecbd38 --- /dev/null +++ b/modules/fmops/qna-rag/app.py @@ -0,0 +1,74 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import os + +import aws_cdk +from aws_cdk import App +from stack import RAGResources + + +def _param(name: str) -> str: + return f"SEEDFARMER_PARAMETER_{name}" + + +project_name = os.getenv("SEEDFARMER_PROJECT_NAME", "") +deployment_name = os.getenv("SEEDFARMER_DEPLOYMENT_NAME", "") +module_name = os.getenv("SEEDFARMER_MODULE_NAME", "") +app_prefix = f"{project_name}-{deployment_name}-{module_name}" +vpc_id = os.getenv(_param("VPC_ID")) +cognito_pool_id = os.getenv(_param("COGNITO_POOL_ID")) +os_domain_endpoint = os.getenv(_param("OS_DOMAIN_ENDPOINT")) +os_domain_port = os.getenv(_param("OS_DOMAIN_PORT"), "443") +os_security_group_id = os.getenv(_param("OS_SECURITY_GROUP_ID")) +input_asset_bucket_name = os.getenv(_param("INPUT_ASSET_BUCKET")) + +if not vpc_id: + raise ValueError("Missing input parameter vpc-id") + +if not cognito_pool_id: + raise ValueError("Missing input parameter cognito-pool-id") + +if not os_domain_endpoint: + raise ValueError("Missing input parameter os-domain-endpoint") + +if not os_security_group_id: + raise ValueError("Missing input parameter os-security-group-id") + +app = App() + +stack = RAGResources( + scope=app, + id=app_prefix, + vpc_id=vpc_id, + cognito_pool_id=cognito_pool_id, + os_domain_endpoint=os_domain_endpoint, + os_domain_port=os_domain_port, + os_security_group_id=os_security_group_id, + os_index_name="rag-index", + input_asset_bucket_name=input_asset_bucket_name, + env=aws_cdk.Environment( + account=os.environ["CDK_DEFAULT_ACCOUNT"], + region=os.environ["CDK_DEFAULT_REGION"], + ), +) + +assert stack.rag_ingest_resource.s3_input_assets_bucket is not None +assert stack.rag_ingest_resource.s3_processed_assets_bucket is not None + +aws_cdk.CfnOutput( + scope=stack, + id="metadata", + value=stack.to_json_string( + { + "IngestionGraphqlApiId": stack.rag_ingest_resource.graphql_api.api_id, + "IngestionGraphqlArn": stack.rag_ingest_resource.graphql_api.arn, + "QnAGraphqlApiId": stack.rag_resource.graphql_api.api_id, + "QnAGraphqlArn": stack.rag_resource.graphql_api.arn, + "InputAssetBucket": stack.rag_ingest_resource.s3_input_assets_bucket.bucket_name, + "ProcessedInputBucket": stack.rag_ingest_resource.s3_processed_assets_bucket.bucket_name, + } + ), +) + +app.synth(force=True) diff --git a/modules/fmops/qna-rag/deployspec.yaml b/modules/fmops/qna-rag/deployspec.yaml new file mode 100644 index 00000000..5b17ea82 --- /dev/null +++ b/modules/fmops/qna-rag/deployspec.yaml @@ -0,0 +1,21 @@ +publishGenericEnvVariables: true +deploy: + phases: + install: + commands: + - npm install -g aws-cdk@2.137.0 + - pip install -r requirements.txt + build: + commands: + - cdk deploy --require-approval never --progress events --app "python app.py" --outputs-file ./cdk-exports.json + # Export metadata + - seedfarmer metadata convert -f cdk-exports.json || true +destroy: + phases: + install: + commands: + - npm install -g aws-cdk@2.137.0 + - pip install -r requirements.txt + build: + commands: + - cdk destroy --force --app "python app.py" \ No newline at end of file diff --git a/modules/fmops/qna-rag/docs/_static/architecture.png b/modules/fmops/qna-rag/docs/_static/architecture.png new file mode 100644 index 00000000..7ca9a9aa Binary files /dev/null and b/modules/fmops/qna-rag/docs/_static/architecture.png differ diff --git a/modules/fmops/qna-rag/docs/_static/ingestion_architecture.png b/modules/fmops/qna-rag/docs/_static/ingestion_architecture.png new file mode 100644 index 00000000..a1c7fa2d Binary files /dev/null and b/modules/fmops/qna-rag/docs/_static/ingestion_architecture.png differ diff --git a/modules/fmops/qna-rag/requirements.in b/modules/fmops/qna-rag/requirements.in new file mode 100644 index 00000000..90cf6f5c --- /dev/null +++ b/modules/fmops/qna-rag/requirements.in @@ -0,0 +1,4 @@ +aws-cdk-lib==2.137.0 +boto3~=1.34.84 +attrs==23.2.0 +cdklabs-generative-ai-cdk-constructs==0.1.146 diff --git a/modules/fmops/qna-rag/requirements.txt b/modules/fmops/qna-rag/requirements.txt new file mode 100644 index 00000000..bb39bcd4 --- /dev/null +++ b/modules/fmops/qna-rag/requirements.txt @@ -0,0 +1,86 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile --output-file=requirements.txt requirements.in +# +attrs==23.2.0 + # via + # -r requirements.in + # cattrs + # jsii +aws-cdk-asset-awscli-v1==2.2.202 + # via aws-cdk-lib +aws-cdk-asset-kubectl-v20==2.1.2 + # via aws-cdk-lib +aws-cdk-asset-node-proxy-agent-v6==2.0.3 + # via aws-cdk-lib +aws-cdk-lib==2.137.0 + # via + # -r requirements.in + # cdk-nag + # cdklabs-generative-ai-cdk-constructs +boto3==1.34.108 + # via -r requirements.in +botocore==1.34.108 + # via + # boto3 + # s3transfer +cattrs==23.2.3 + # via jsii +cdk-nag==2.28.118 + # via cdklabs-generative-ai-cdk-constructs +cdklabs-generative-ai-cdk-constructs==0.1.145 + # via -r requirements.in +constructs==10.3.0 + # via + # aws-cdk-lib + # cdk-nag + # cdklabs-generative-ai-cdk-constructs +importlib-resources==6.4.0 + # via jsii +jmespath==1.0.1 + # via + # boto3 + # botocore +jsii==1.98.0 + # via + # aws-cdk-asset-awscli-v1 + # aws-cdk-asset-kubectl-v20 + # aws-cdk-asset-node-proxy-agent-v6 + # aws-cdk-lib + # cdk-nag + # cdklabs-generative-ai-cdk-constructs + # constructs +publication==0.0.3 + # via + # aws-cdk-asset-awscli-v1 + # aws-cdk-asset-kubectl-v20 + # aws-cdk-asset-node-proxy-agent-v6 + # aws-cdk-lib + # cdk-nag + # cdklabs-generative-ai-cdk-constructs + # constructs + # jsii +python-dateutil==2.9.0.post0 + # via + # botocore + # jsii +s3transfer==0.10.1 + # via boto3 +six==1.16.0 + # via python-dateutil +typeguard==2.13.3 + # via + # aws-cdk-asset-awscli-v1 + # aws-cdk-asset-kubectl-v20 + # aws-cdk-asset-node-proxy-agent-v6 + # aws-cdk-lib + # cdk-nag + # cdklabs-generative-ai-cdk-constructs + # constructs + # jsii +typing-extensions==4.11.0 + # via jsii +urllib3==1.26.17 + # via botocore diff --git a/modules/fmops/qna-rag/stack.py b/modules/fmops/qna-rag/stack.py new file mode 100644 index 00000000..971baaf9 --- /dev/null +++ b/modules/fmops/qna-rag/stack.py @@ -0,0 +1,103 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +from constructs import Construct +from cdklabs.generative_ai_cdk_constructs import RagAppsyncStepfnOpensearch +from cdklabs.generative_ai_cdk_constructs import QaAppsyncOpensearch +from aws_cdk import Stack +from aws_cdk import aws_ec2 as ec2 +from aws_cdk import aws_s3 as s3 +from typing import Optional +from aws_cdk import ( + aws_opensearchservice as os, + aws_cognito as cognito, +) + + +class RAGResources(Stack): + def __init__( + self, + scope: Construct, + id: str, + vpc_id: str, + cognito_pool_id: str, + os_domain_endpoint: str, + os_domain_port: str, + os_security_group_id: str, + os_index_name: str, + input_asset_bucket_name: Optional[str], + **kwargs, + ) -> None: + super().__init__( + scope, + id, + description=" This stack creates resources for the LLM - QA RAG ", + **kwargs, + ) + + # get an existing OpenSearch provisioned cluster + os_domain = os.Domain.from_domain_endpoint( + self, + "osdomain", + domain_endpoint="https://" + os_domain_endpoint, + ) + self.os_domain = os_domain + # get vpc from vpc id + vpc = ec2.Vpc.from_lookup( + self, + "VPC", + vpc_id=vpc_id, + ) + + # get an existing userpool + cognito_pool_id = cognito_pool_id + user_pool_loaded = cognito.UserPool.from_user_pool_id( + self, + "myuserpool", + user_pool_id=cognito_pool_id, + ) + + if input_asset_bucket_name: + input_asset_bucket = s3.Bucket.from_bucket_name( + self, "input-assets-bucket", input_asset_bucket_name + ) + else: + input_asset_bucket = None + # 1. Create Ingestion pipeline + rag_ingest_resource = RagAppsyncStepfnOpensearch( + self, + "RagAppsyncStepfnOpensearch", + existing_vpc=vpc, + existing_opensearch_domain=os_domain, + open_search_index_name=os_index_name, + cognito_user_pool=user_pool_loaded, + existing_input_assets_bucket_obj=input_asset_bucket, + ) + + self.security_group_id = rag_ingest_resource.security_group.security_group_id + + self.rag_ingest_resource = rag_ingest_resource + # 2. create question and answer pipeline + rag_qa_source = QaAppsyncOpensearch( + self, + "QaAppsyncOpensearch", + existing_vpc=vpc, + existing_opensearch_domain=os_domain, + open_search_index_name=os_index_name, + cognito_user_pool=user_pool_loaded, + existing_input_assets_bucket_obj=rag_ingest_resource.s3_processed_assets_bucket, + existing_security_group=rag_ingest_resource.security_group, + ) + + security_group = rag_qa_source.security_group + + os_security_group = ec2.SecurityGroup.from_security_group_id( + self, "OSSecurityGroup", os_security_group_id + ) + os_security_group.add_ingress_rule( + peer=security_group, + connection=ec2.Port.tcp(int(os_domain_port)), + description="Allow inbound HTTPS to open search from embeddings lambda and question answering lambda", + ) + + self.rag_resource = rag_qa_source diff --git a/modules/fmops/qna-rag/tests/__init__.py b/modules/fmops/qna-rag/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/fmops/qna-rag/tests/test_app.py b/modules/fmops/qna-rag/tests/test_app.py new file mode 100644 index 00000000..9c23a10b --- /dev/null +++ b/modules/fmops/qna-rag/tests/test_app.py @@ -0,0 +1,56 @@ +import os +import sys + +import pytest + + +@pytest.fixture(scope="function") +def stack_defaults() -> None: + os.environ["SEEDFARMER_PROJECT_NAME"] = "test-project" + os.environ["SEEDFARMER_DEPLOYMENT_NAME"] = "test-deployment" + os.environ["SEEDFARMER_MODULE_NAME"] = "test-module" + os.environ["CDK_DEFAULT_ACCOUNT"] = "111111111111" + os.environ["CDK_DEFAULT_REGION"] = "us-east-1" + + os.environ["SEEDFARMER_PARAMETER_VPC_ID"] = "vpc-12345" + os.environ["SEEDFARMER_PARAMETER_COGNITO_POOL_ID"] = "12345" + os.environ["SEEDFARMER_PARAMETER_OS_DOMAIN_ENDPOINT"] = "sample-endpoint.com" + os.environ["SEEDFARMER_PARAMETER_OS_SECURITY_GROUP_ID"] = "sg-1234abcd" + + # Unload the app import so that subsequent tests don't reuse + if "app" in sys.modules: + del sys.modules["app"] + + +def test_app(stack_defaults): # type: ignore[no-untyped-def] + import app # noqa: F401 + + +def test_vpc_id(stack_defaults): + del os.environ["SEEDFARMER_PARAMETER_VPC_ID"] + + with pytest.raises(ValueError, match="Missing input parameter vpc-id"): + import app # noqa: F401 + + +def test_cognito_pool_id(stack_defaults): + del os.environ["SEEDFARMER_PARAMETER_COGNITO_POOL_ID"] + + with pytest.raises(ValueError, match="Missing input parameter cognito-pool-id"): + import app # noqa: F401 + + +def test_os_domain_endpoint(stack_defaults): + del os.environ["SEEDFARMER_PARAMETER_OS_DOMAIN_ENDPOINT"] + + with pytest.raises(ValueError, match="Missing input parameter os-domain-endpoint"): + import app # noqa: F401 + + +def test_os_security_group(stack_defaults): + del os.environ["SEEDFARMER_PARAMETER_OS_SECURITY_GROUP_ID"] + + with pytest.raises( + ValueError, match="Missing input parameter os-security-group-id" + ): + import app # noqa: F401 diff --git a/modules/fmops/qna-rag/tests/test_stack.py b/modules/fmops/qna-rag/tests/test_stack.py new file mode 100644 index 00000000..cc0a2fd7 --- /dev/null +++ b/modules/fmops/qna-rag/tests/test_stack.py @@ -0,0 +1,59 @@ +import os +import sys + +import aws_cdk as cdk +import pytest +from aws_cdk.assertions import Template + + +@pytest.fixture(scope="function") +def stack_defaults() -> None: + os.environ["CDK_DEFAULT_ACCOUNT"] = "111111111111" + os.environ["CDK_DEFAULT_REGION"] = "us-east-1" + + # Unload the app import so that subsequent tests don't reuse + if "stack" in sys.modules: + del sys.modules["stack"] + + +@pytest.fixture(scope="function") +def stack_model_package_input() -> cdk.Stack: + import stack + + app = cdk.App() + + project_name = "test-project" + deployment_name = "test-deployment" + module_name = "test-module" + + app_prefix = f"{project_name}-{deployment_name}-{module_name}" + vpc_id = "vpc-123" + cognito_pool_id = "us-east-1_XXXXX" + os_domain_endpoint = "sample-endpoint.com" + os_security_group_id = "sg-a1b2c3d4" + + return stack.RAGResources( + scope=app, + id=app_prefix, + vpc_id=vpc_id, + cognito_pool_id=cognito_pool_id, + os_domain_endpoint=os_domain_endpoint, + os_domain_port="443", + os_security_group_id=os_security_group_id, + os_index_name="sample", + input_asset_bucket_name="input-bucket", + env=cdk.Environment( + account="111111111111", + region="us-east-1", + ), + ) + + +@pytest.fixture(params=["stack_model_package_input"], scope="function") +def stack(request, stack_model_package_input) -> cdk.Stack: # type: ignore[no-untyped-def] + return request.getfixturevalue(request.param) # type: ignore[no-any-return] + + +def test_synthesize_stack(stack: cdk.Stack) -> None: + template = Template.from_stack(stack) + template.resource_count_is("AWS::AppSync::Resolver", 4)