├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cloudformation.yaml ├── LICENSE ├── README.md ├── TROUBLESHOOTING.md ├── docs ├── Architecture.jpg ├── Architecture.png ├── arch.jpg ├── arch_idc.png ├── cfn_update.png ├── iamdic_2.png ├── iamidc_3.png ├── iamidc_4.png ├── iamidcapp_1.png ├── iamidcapp_10.png ├── iamidcapp_11.png ├── iamidcapp_5.png ├── iamidcapp_6.png ├── iamidcapp_7.png ├── iamidcapp_8.png └── properties.png ├── requirements.txt ├── ruff.toml ├── scripts └── self_sign.sh └── src ├── app.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | bedrock-python-sdk 2 | bedrock-python-sdk.zip 3 | verification 4 | 5 | __pycache__/ 6 | .DS_Store 7 | 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | wheels/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .nox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | *.py,cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | cover/ 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | db.sqlite3-journal 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | 81 | # PyBuilder 82 | .pybuilder/ 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | # For a library or package, you might want to ignore these files since the code is 94 | # intended to run in multiple environments; otherwise, check them in: 95 | # .python-version 96 | 97 | # pipenv 98 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 99 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 100 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 101 | # install all needed dependencies. 102 | #Pipfile.lock 103 | 104 | # poetry 105 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 106 | # This is especially recommended for binary packages to ensure reproducibility, and is more 107 | # commonly ignored for libraries. 108 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 109 | #poetry.lock 110 | 111 | # pdm 112 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 113 | #pdm.lock 114 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 115 | # in version control. 116 | # https://pdm.fming.dev/#use-with-ide 117 | .pdm.toml 118 | 119 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 120 | __pypackages__/ 121 | 122 | # Celery stuff 123 | celerybeat-schedule 124 | celerybeat.pid 125 | 126 | # SageMath parsed files 127 | *.sage.py 128 | 129 | # Environments 130 | .env 131 | .venv 132 | env/ 133 | venv/ 134 | ENV/ 135 | env.bak/ 136 | venv.bak/ 137 | 138 | # Spyder project settings 139 | .spyderproject 140 | .spyproject 141 | 142 | # Rope project settings 143 | .ropeproject 144 | 145 | # mkdocs documentation 146 | /site 147 | 148 | # mypy 149 | .mypy_cache/ 150 | .dmypy.json 151 | dmypy.json 152 | 153 | # Pyre type checker 154 | .pyre/ 155 | 156 | # pytype static type analyzer 157 | .pytype/ 158 | 159 | # Cython debug symbols 160 | cython_debug/ 161 | 162 | # PyCharm 163 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 164 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 165 | # and can be added to the global gitignore or merged into this file. For a more nuclear 166 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 167 | #.idea/ 168 | 169 | 170 | local.md 171 | docs/arch.drawio 172 | reportliner.json -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Cloudformation.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Description: 'CloudFormation for the blog on GenAI with QBusiness' 3 | Parameters: 4 | 5 | LatestAmiId: 6 | Description: EC2 machine image 7 | Type: 'AWS::SSM::Parameter::Value' 8 | Default: '/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64' 9 | VpcId: 10 | Description: ID of the existing VPC 11 | Type: AWS::EC2::VPC::Id 12 | ConstraintDescription: must be the ID of an existing VPC 13 | PublicSubnetIds: 14 | Description: List of IDs of existing public subnets. Please select at least two subnets 15 | Type: List 16 | CertificateARN: 17 | Description: Certificate that needs to be added to the Load Balancer 18 | Type: String 19 | AuthName: 20 | Type: String 21 | Description: Unique Auth Name for Cognito Resources 22 | AllowedPattern: ^[a-z0-9]+$ 23 | ConstraintDescription: May only include lowercase, alphanumeric characters 24 | QApplicationId: 25 | Type: String 26 | Description: Q Application Id 27 | IdcApplicationArn: 28 | Type: String 29 | Description: Identity Center customer application ARN. 30 | Default: "" 31 | 32 | Resources: 33 | QManagedPolicy: 34 | Type: AWS::IAM::ManagedPolicy 35 | Properties: 36 | PolicyDocument: 37 | Version: '2012-10-17' 38 | Statement: 39 | - Sid: AllowQChat 40 | Effect: Allow 41 | Action: 42 | - "qbusiness:ChatSync" 43 | Resource: !Sub "arn:${AWS::Partition}:qbusiness:${AWS::Region}:${AWS::AccountId}:application/${QApplicationId}" 44 | 45 | QServiceRole: 46 | Type: AWS::IAM::Role 47 | Properties: 48 | AssumeRolePolicyDocument: 49 | Version: 2012-10-17 50 | Statement: 51 | - Effect: Allow 52 | Principal: 53 | AWS: 54 | - !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:root 55 | Action: 56 | - sts:AssumeRole 57 | - sts:SetContext 58 | Condition: 59 | ArnEquals: 60 | "aws:PrincipalArn": !GetAtt EC2ServiceRole.Arn 61 | Path: / 62 | ManagedPolicyArns: 63 | - !Ref QManagedPolicy 64 | 65 | EC2ServiceRole: 66 | Type: AWS::IAM::Role 67 | Properties: 68 | AssumeRolePolicyDocument: 69 | Version: 2012-10-17 70 | Statement: 71 | - Effect: Allow 72 | Principal: 73 | Service: 74 | - ec2.amazonaws.com 75 | Action: 76 | - sts:AssumeRole 77 | Path: / 78 | ManagedPolicyArns: 79 | - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore 80 | 81 | EC2ServicePolicy: 82 | Type: AWS::IAM::ManagedPolicy 83 | Metadata: 84 | guard: 85 | SuppressedRules: 86 | - IAM_POLICYDOCUMENT_NO_WILDCARD_RESOURCE # CreateTokenWithIAM requires wildcard 87 | Properties: 88 | Roles: 89 | - !Ref EC2ServiceRole 90 | PolicyDocument: 91 | Version: '2012-10-17' 92 | Statement: 93 | - Sid: AllowAssumeQRole 94 | Effect: Allow 95 | Action: 96 | - "sts:AssumeRole" 97 | - "sts:SetContext" 98 | Resource: !GetAtt QServiceRole.Arn 99 | - Sid: AllowTokenExchange 100 | Effect: Allow 101 | Action: 102 | - "sso-oauth:CreateTokenWithIAM" 103 | Resource: "*" 104 | - Sid: AllowAppConfig 105 | Effect: Allow 106 | Action: 107 | - "appconfig:StartConfigurationSession" 108 | - "appconfig:GetLatestConfiguration" 109 | Resource: 110 | - !Sub "arn:${AWS::Partition}:appconfig:${AWS::Region}:${AWS::AccountId}:application/${AppConfig}/environment/${AppConfigEnvironment}/configuration/${AppConfigConfigProfile}" 111 | 112 | AppConfig: 113 | Type: AWS::AppConfig::Application 114 | Properties: 115 | Name: qcustomwebui 116 | 117 | AppConfigEnvironment: 118 | Type: AWS::AppConfig::Environment 119 | Properties: 120 | ApplicationId: !Ref AppConfig 121 | Name: qcustomwebui-env 122 | 123 | AppConfigConfigProfile: 124 | Type: AWS::AppConfig::ConfigurationProfile 125 | Properties: 126 | ApplicationId: !Ref AppConfig 127 | Name: qcustomwebui-config 128 | LocationUri: "hosted" 129 | 130 | AppConfigConfigVersion: 131 | Type: AWS::AppConfig::HostedConfigurationVersion 132 | Properties: 133 | ApplicationId: !Ref AppConfig 134 | ConfigurationProfileId: !Ref AppConfigConfigProfile 135 | ContentType: "application/json" 136 | Content: !Sub | 137 | { 138 | "AmazonQAppId": "${QApplicationId}", 139 | "IamRoleArn": "${QServiceRole.Arn}", 140 | "Region": "${AWS::Region}", 141 | "IdcApplicationArn": "${IdcApplicationArn}", 142 | "OAuthConfig": { 143 | "ClientId": "${UserPoolClient}", 144 | "ExternalDns": "${LowerCaseFqdn.Output}", 145 | "CognitoDomain" : "${UserPoolDomain}.auth.${AWS::Region}.amazoncognito.com" 146 | } 147 | } 148 | AppConfigDeployment: 149 | Type: AWS::AppConfig::Deployment 150 | Properties: 151 | ApplicationId: !Ref AppConfig 152 | ConfigurationProfileId: !Ref AppConfigConfigProfile 153 | ConfigurationVersion: !GetAtt AppConfigConfigVersion.VersionNumber 154 | EnvironmentId: !Ref AppConfigEnvironment 155 | DeploymentStrategyId: !Ref AppConfigDeploymentStrategy 156 | 157 | AppConfigDeploymentStrategy: 158 | Type: AWS::AppConfig::DeploymentStrategy 159 | Properties: 160 | DeploymentDurationInMinutes: 0 161 | FinalBakeTimeInMinutes: 0 162 | GrowthFactor: 100 163 | Name: "Quick deployment" 164 | ReplicateTo: "NONE" 165 | GrowthType: "LINEAR" 166 | # EC2 and ALB Security Groups 167 | ELBSecurityGroup: 168 | Type: AWS::EC2::SecurityGroup 169 | Metadata: 170 | guard: 171 | SuppressedRules: 172 | - EC2_SECURITY_GROUP_INGRESS_OPEN_TO_WORLD_RULE # This SG only applies to Internet facing ALB 173 | - SECURITY_GROUP_INGRESS_CIDR_NON_32_RULE 174 | - SECURITY_GROUP_MISSING_EGRESS_RULE 175 | Properties: 176 | GroupDescription: ELB Security Group 177 | VpcId: !Ref VpcId 178 | SecurityGroupIngress: 179 | - IpProtocol: tcp 180 | FromPort: 443 181 | ToPort: 443 182 | CidrIp: 0.0.0.0/0 183 | Description: HTTPS from Internet 184 | - IpProtocol: tcp 185 | FromPort: 80 186 | ToPort: 80 187 | CidrIp: 0.0.0.0/0 188 | Description: HTTP from Internet 189 | 190 | ELBSecurityGroupEgress: 191 | Type: AWS::EC2::SecurityGroupEgress 192 | Properties: 193 | Description: Allow outbound traffic to EC2 Instance 194 | GroupId: !Ref ELBSecurityGroup 195 | IpProtocol: "tcp" 196 | FromPort: 8080 197 | ToPort: 8080 198 | DestinationSecurityGroupId: !Ref SecurityGroup 199 | 200 | SecurityGroup: 201 | Type: AWS::EC2::SecurityGroup 202 | Metadata: 203 | guard: 204 | SuppressedRules: 205 | - SECURITY_GROUP_MISSING_EGRESS_RULE 206 | Properties: 207 | GroupDescription: EC2 Security group 208 | VpcId: !Ref VpcId 209 | SecurityGroupIngress: 210 | - IpProtocol: tcp 211 | FromPort: 8080 212 | ToPort: 8080 213 | SourceSecurityGroupId: !Ref ELBSecurityGroup 214 | Description: Allow inbound traffic from ALB 215 | 216 | SecurityGroupEgress: 217 | Type: AWS::EC2::SecurityGroupEgress 218 | Metadata: 219 | guard: 220 | SuppressedRules: 221 | - EC2_SECURITY_GROUP_EGRESS_OPEN_TO_WORLD_RULE 222 | - SECURITY_GROUP_EGRESS_ALL_PROTOCOLS_RULE 223 | Properties: 224 | Description: Allow all outbound traffic 225 | GroupId: !Ref SecurityGroup 226 | IpProtocol: "-1" 227 | CidrIp: 0.0.0.0/0 228 | 229 | 230 | EC2InstanceProfile: 231 | Type: AWS::IAM::InstanceProfile 232 | Properties: 233 | Path: "/" 234 | Roles: 235 | - !Ref EC2ServiceRole 236 | 237 | AutoScalingGroup: 238 | Type: AWS::AutoScaling::AutoScalingGroup 239 | Properties: 240 | MaxSize: 1 241 | MinSize: 1 242 | DesiredCapacity: 1 243 | TargetGroupARNs: 244 | - !Ref EC2TargetGroup 245 | HealthCheckType: ELB 246 | HealthCheckGracePeriod: 180 247 | VPCZoneIdentifier: !Ref PublicSubnetIds 248 | 249 | LaunchTemplate: 250 | Version: !GetAtt LaunchTemplate.LatestVersionNumber 251 | LaunchTemplateId: !Ref LaunchTemplate 252 | Tags: 253 | - Key: Name 254 | Value: Custom Q UI 255 | PropagateAtLaunch: true 256 | 257 | LaunchTemplate: 258 | Type: AWS::EC2::LaunchTemplate 259 | Properties: 260 | LaunchTemplateData: 261 | NetworkInterfaces: 262 | - DeviceIndex: 0 263 | AssociatePublicIpAddress: true 264 | SubnetId: !Select [0, !Ref PublicSubnetIds] 265 | Groups: 266 | - !Ref SecurityGroup 267 | EbsOptimized: true 268 | ImageId: !Ref 'LatestAmiId' 269 | InstanceType: t3.micro 270 | IamInstanceProfile: 271 | Arn: !GetAtt EC2InstanceProfile.Arn 272 | UserData: 273 | Fn::Base64: !Sub | 274 | #!/bin/bash 275 | max_attempts=5 276 | attempt_num=1 277 | success=false 278 | while [ $success = false ] && [ $attempt_num -le $max_attempts ]; do 279 | echo "Trying dnf install" 280 | dnf -y install python3.11 python3.11-pip git 281 | # Check the exit code of the command 282 | if [ $? -eq 0 ]; then 283 | echo "Yum install succeeded" 284 | success=true 285 | else 286 | echo "Attempt $attempt_num failed. Sleeping for 3 seconds and trying again..." 287 | sleep 3 288 | ((attempt_num++)) 289 | fi 290 | done 291 | max_attempts=5 292 | attempt_num=1 293 | success=false 294 | while [ $success = false ] && [ $attempt_num -le $max_attempts ]; do 295 | echo "Trying dnf install" 296 | dnf -y install https://s3.amazonaws.com/aws-appconfig-downloads/aws-appconfig-agent/linux/x86_64/latest/aws-appconfig-agent.rpm 297 | # Check the exit code of the command 298 | if [ $? -eq 0 ]; then 299 | echo "Yum install succeeded" 300 | success=true 301 | else 302 | echo "Attempt $attempt_num failed. Sleeping for 3 seconds and trying again..." 303 | sleep 3 304 | ((attempt_num++)) 305 | fi 306 | done 307 | mkdir /etc/systemd/system/aws-appconfig-agent.service.d 308 | echo "[Service]" > /etc/systemd/system/aws-appconfig-agent.service.d/overrides.conf 309 | echo "Environment=SERVICE_REGION=${AWS::Region}" >> /etc/systemd/system/aws-appconfig-agent.service.d/overrides.conf 310 | systemctl daemon-reload 311 | systemctl enable aws-appconfig-agent 312 | systemctl restart aws-appconfig-agent 313 | cd /opt 314 | git clone https://github.com/aws-samples/custom-web-experience-with-amazon-q-business.git 315 | cd custom-web-experience-with-amazon-q-business/ 316 | pip3.11 install virtualenv 317 | python3.11 -m virtualenv venv 318 | venv/bin/pip install -r requirements.txt 319 | APPCONFIG_APP_NAME=${AppConfig} APPCONFIG_ENV_NAME=${AppConfigEnvironment} APPCONFIG_CONF_NAME=${AppConfigConfigProfile} nohup venv/bin/streamlit run src/app.py --server.port=8080 > logs.txt & 320 | 321 | # Target Group, Listener and Application Load Balancer 322 | EC2TargetGroup: 323 | Type: AWS::ElasticLoadBalancingV2::TargetGroup 324 | Properties: 325 | HealthCheckIntervalSeconds: 90 326 | HealthCheckProtocol: HTTP 327 | HealthCheckTimeoutSeconds: 45 328 | HealthyThresholdCount: 5 329 | Matcher: 330 | HttpCode: '200' 331 | Name: EC2TargetGroup 332 | Port: 8080 333 | Protocol: HTTP 334 | TargetGroupAttributes: 335 | - Key: deregistration_delay.timeout_seconds 336 | Value: '60' 337 | UnhealthyThresholdCount: 3 338 | VpcId: !Ref VpcId 339 | 340 | UserPool: 341 | Type: AWS::Cognito::UserPool 342 | Metadata: 343 | guard: 344 | SuppressedRules: 345 | - COGNITO_USER_POOL_MFA_CONFIGURATION_RULE # Not required for the demo 346 | Properties: 347 | UserPoolName: !Sub ${AuthName}-user-pool 348 | AutoVerifiedAttributes: 349 | - email 350 | MfaConfiguration: "OFF" 351 | Schema: 352 | - Name: email 353 | AttributeDataType: String 354 | Mutable: false 355 | Required: true 356 | 357 | UserPoolClient: 358 | Type: AWS::Cognito::UserPoolClient 359 | Properties: 360 | ClientName: !Sub ${AuthName}-client 361 | GenerateSecret: false 362 | UserPoolId: !Ref UserPool 363 | AllowedOAuthFlowsUserPoolClient: True 364 | AllowedOAuthFlows: 365 | - code 366 | AllowedOAuthScopes: 367 | - openid 368 | SupportedIdentityProviders: 369 | - COGNITO 370 | CallbackURLs: 371 | - !Sub "https://${LowerCaseFqdn.Output}/component/streamlit_oauth.authorize_button/index.html" 372 | 373 | UserPoolDomain: 374 | Type: AWS::Cognito::UserPoolDomain 375 | Properties: 376 | UserPoolId: !Ref UserPool 377 | Domain: !Sub ${AuthName}-dns-testname 378 | 379 | ALBListener2: 380 | Type: AWS::ElasticLoadBalancingV2::Listener 381 | Metadata: 382 | guard: 383 | SuppressedRules: 384 | - ELBV2_ACM_CERTIFICATE_REQUIRED # Certificate is loaded externally for the demo 385 | Properties: 386 | LoadBalancerArn: !Ref ApplicationLoadBalancer 387 | Port: 443 388 | Protocol: HTTPS 389 | SslPolicy: ELBSecurityPolicy-TLS13-1-2-2021-06 390 | Certificates: 391 | - CertificateArn: !Ref CertificateARN 392 | DefaultActions: 393 | - Type: forward 394 | TargetGroupArn: !Ref EC2TargetGroup 395 | Order: 1 396 | 397 | ALBListener80: 398 | Type: AWS::ElasticLoadBalancingV2::Listener 399 | Metadata: 400 | guard: 401 | SuppressedRules: 402 | - ELBV2_LISTENER_PROTOCOL_RULE # Not required for the demo 403 | - ELBV2_LISTENER_SSL_POLICY_RULE # NO SSL Policy for an HTTP listener 404 | Properties: 405 | LoadBalancerArn: !Ref ApplicationLoadBalancer 406 | Port: 80 407 | Protocol: HTTP 408 | DefaultActions: 409 | - Order: 1 410 | RedirectConfig: 411 | Protocol: "HTTPS" 412 | Port: "443" 413 | Host: "#{host}" 414 | Path: "/#{path}" 415 | Query: "#{query}" 416 | StatusCode: "HTTP_301" 417 | Type: "redirect" 418 | 419 | ApplicationLoadBalancer: 420 | Type: AWS::ElasticLoadBalancingV2::LoadBalancer 421 | Metadata: 422 | guard: 423 | SuppressedRules: 424 | - ELB_DELETION_PROTECTION_ENABLED # Not required for the demo 425 | - ELBV2_ACCESS_LOGGING_RULE # Not required for the demo 426 | Properties: 427 | Scheme: internet-facing 428 | Subnets: 429 | - !Select [0, !Ref PublicSubnetIds] 430 | - !Select [1, !Ref PublicSubnetIds] 431 | SecurityGroups: 432 | - !Ref ELBSecurityGroup 433 | 434 | LowerCaseFqdn: 435 | Type: Custom::LowerCaseFqdn 436 | Properties: 437 | ServiceToken: !GetAtt LowerCaseFunction.Arn 438 | Input: !GetAtt ApplicationLoadBalancer.DNSName 439 | 440 | LowerCaseFunction: 441 | Type: AWS::Lambda::Function 442 | Metadata: 443 | guard: 444 | SuppressedRules: 445 | - LAMBDA_DLQ_CHECK # This a synchronous call no need for DLQ 446 | - LAMBDA_INSIDE_VPC # No need for VPC 447 | - LAMBDA_CONCURRENCY_CHECK # Not required for the demo 448 | Properties: 449 | Handler: index.handler 450 | Role: !GetAtt LowerCaseRole.Arn 451 | Code: 452 | ZipFile: | 453 | import cfnresponse 454 | def error_handler(func): 455 | def wrapper(*args, **kwargs): 456 | try: 457 | return func(*args, **kwargs) 458 | except Exception as e: 459 | logger.error(e) 460 | cfnresponse.send(args[0], args[1], cfnresponse.FAILED, {}) 461 | return None 462 | return wrapper 463 | 464 | @error_handler 465 | def handler(event, context): 466 | if event["RequestType"] in ["Create", "Update"]: 467 | response = {} 468 | cfnresponse.send(event, context, cfnresponse.SUCCESS, {"Output": event['ResourceProperties']['Input'].lower()}) 469 | if event["RequestType"] == "Delete": 470 | cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) 471 | Runtime: python3.12 472 | Timeout: 60 473 | 474 | LowerCaseRole: 475 | Type: AWS::IAM::Role 476 | Properties: 477 | AssumeRolePolicyDocument: 478 | Version: 2012-10-17 479 | Statement: 480 | - Effect: Allow 481 | Principal: 482 | Service: 483 | - lambda.amazonaws.com 484 | Action: 485 | - sts:AssumeRole 486 | Path: / 487 | ManagedPolicyArns: 488 | - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 489 | 490 | Outputs: 491 | URL: 492 | Description: URL to access the Streamlit APP 493 | Value: 494 | !Sub https://${ApplicationLoadBalancer.DNSName} 495 | TrustedIssuerUrl: 496 | Description: Endpoint of the trusted issuer to setup Identity Center 497 | Value: !GetAtt UserPool.ProviderURL 498 | Audience: 499 | Description: Audience to setup customer application in Identity Center 500 | Value: !Ref UserPoolClient 501 | RoleArn: 502 | Description: "ARN of the IAM role required to setup token exchange in Identity Center" 503 | Value: !GetAtt EC2ServiceRole.Arn 504 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Custom Web Experience with Amazon Q Business 2 | 3 | | :zap: If you created a new Amazon Q Business application on or after April 30th, 2024, you can now set up a custom UI using the updated instructions provided below. 4 | |-----------------------------------------| 5 | 6 | **Note:** The instructions provided in this guide are specific to Cognito, but they should also work for other OIDC 2.0 compliant Identity Providers (IdPs) with minor adjustments. 7 | 8 | Customers often want the ability to integrate custom functionalities into the Amazon Q user interface, such as handling feedback, using corporate colors and templates, custom login, and reducing context switching by integrating the user interface into a single platform. The code repo will show how to integrate a custom UI on Amazon Q using Amazon Cognito for user authentication and Amazon Q SDK to invoke chatbot application programmatically, through [chat_sync API](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/qbusiness/client/chat_sync.html). 9 | 10 | Architecture Diagram 11 | 12 | 13 | 14 | 👨‍💻 The workflow includes the following steps: 15 | 1. First the user accesses the chatbot application, which is hosted behind an Application Load Balancer. 16 | 17 | 2. The user is prompted to log with Cognito 18 | 19 | 3. The UI application exchanges the token from Cognito with an IAM Identity Center token with the scope for Amazon Q 20 | 21 | 4. The UI applications assumes an IAM role and retrieve an AWS Session from Secure Token Service (STS), augmented with the IAM Identity Center token to interact with Amazon Q 22 | * Detail flow of token exchange between IAM Identity Center and Idp is explained in below blog posts 23 | 24 | 🔗 [Blog 1](https://aws.amazon.com/blogs/storage/how-to-develop-a-user-facing-data-application-with-iam-identity-center-and-s3-access-grants/) 25 | 26 | 🔗 [Blog 2](https://aws.amazon.com/blogs/storage/how-to-develop-a-user-facing-data-application-with-iam-identity-center-and-s3-access-grants-part-2/) 27 | 28 | 29 | 5. Amazon Q uses the ChatSync API to carry out the conversation. Thanks to the identity-aware session, Amazon Q knows which user it is interacting with. 30 | 31 | * The request uses the following mandatory parameters. 32 | 33 | 1. **applicationId**: The identifier of the Amazon Q application linked to the Amazon Q conversation. 34 | 35 | 2. **userMessage**: An end user message in a conversation. 36 | 37 | * Amazon Q returns the response as a JSON object (detailed in the [Amazon Q documentation](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/qbusiness/client/chat_sync.html)) and below are the few core attributes from the response payload. 38 | 1. **systemMessage**: An AI-generated message in a conversation 39 | 40 | 2. **sourceAttributions**: The source documents used to generate the conversation response .In the RAG (Retrieval Augmentation Generation) this always refer to one or more documents from enterprise knowledge bases which are indexed in Amazon Q. 41 | 42 | 43 | 44 | ## Deploy this solution 45 | 46 | 47 | ### Prerequisites: 48 | Before you deploy this solution, make sure you have the following prerequisites set up: 49 | 50 | - A valid AWS account. 51 | - An AWS Identity and Access Management (IAM) role in the account that has sufficient permissions to create the necessary resources. 52 | If you have administrator access to the account, no action is necessary. 53 | - A TLS certificate created and imported into AWS Certificate Manager (ACM). 54 | For more details, [refer to Importing a certificate](https://docs.aws.amazon.com/acm/latest/userguide/import-certificate-api-cli.html). 55 | If you do not have a public TLS certificate, follow the steps in the next section to learn how to generate a private certificate. 56 | - An existing, working Amazon Q application 57 | - IAM Identity Center, and create few users in Identity Center by configuring their email address and name 58 | 59 | ### Generate Private certificate 60 | 61 | If you already have a TLS certificate, you can skip this section. 62 | However, if you don't have one and want to proceed with running this demo, you can generate a private certificate associated with a domain using the following openssl command: 63 | ``` 64 | openssl req \ 65 | -x509 -nodes -days 365 -sha256 \ 66 | -subj '/C=US/ST=Oregon/L=Portland/CN=sampleexample.com' \ 67 | -newkey rsa:2048 -keyout key.pem -out cert.pem 68 | 69 | aws acm import-certificate --certificate fileb://cert.pem --private-key fileb://key.pem 70 | ``` 71 | 72 | ➡️ Please note that you will receive a warning from your browser when accessing the UI if you did not provide a custom TLS certificate when launching the AWS CloudFormation Stack. The above instructions show you how to create a self-signed certificate, which can be used as a backup, but this is certainly not recommended for production use cases. 73 | 74 | You should obtain a TLS Certificate that has been validated by a certificate authority, import it into AWS Certificate Manager, and reference it when launching the AWS CloudFormation Stack. 75 | 76 | If you wish to continue with the self-signed certificate (for development purposes), you should be able to proceed past the browser warning page. With Chrome, you will see a "Your connection is not private" error message (NET::ERR_CERT_AUTHORITY_INVALID), but by clicking on "Advanced," you should then see a link to proceed. 77 | 78 | 79 | ### 🚀 Deploy this Solution: 80 | 81 | Step 1: Launch the following AWS CloudFormation template to deploy ELB , Cognito User pool , including the EC2 instance to host the webapp. 82 | --------------------------------------------------------------------- 83 | 84 | ⚙️ Provide the following parameters for stack 85 | 86 | • **Stack name** – The name of the CloudFormation stack (for example, AmazonQ-UI-Demo) 87 | 88 | • **AuthName** – A globally unique name to assign to the Amazon Cognito user pool. Please ensure that your domain name does not include any reserved words, such as cognito, aws, or amazon. 89 | 90 | • **CertificateARN** – The CertificateARN generated from the previous step 91 | 92 | • **IdcApplicationArn** – Identity Center customer application ARN , keep it blank on first run as we need to create the cognito user pool as part of this stack to create [IAM Identity Center application with a trusted token issuer](https://docs.aws.amazon.com/singlesignon/latest/userguide/using-apps-with-trusted-token-issuer.html) 93 | 94 | • **PublicSubnetIds** – The IDs of the public subnets that can be used to deploy the EC2 instance and the Application Load Balancer. Please select at least 2 public subnets 95 | 96 | • **QApplicationId** – The existing application ID of Amazon Q 97 | 98 | • **VPCId** – The ID of the existing VPC that can be used to deploy the demo 99 | 100 | 101 | CloudFormation  parameters 102 | 103 | 104 | 🔗 Once the stack is complete , copy the following Key from the Output tab . 105 | ------------------------------------------------ 106 | 107 | **Audience** : Audience to setup customer application in Identity Center 108 | 109 | **RoleArn** : ARN of the IAM role required to setup token exchange in Identity Center 110 | 111 | **TrustedIssuerUrl** : Endpoint of the trusted issuer to setup Identity Center 112 | 113 | **URL** : The Load balancer URL to access the streamlit app 114 | 115 | 116 | Step 2: Create an IAM Identity Center Application 117 | --------------------------------------------------------------------- 118 | 119 | - Navigate to AWS IAM Identity Center, and add a new custom managed application. 120 | 121 | **Select application type** -> then select OAuth2.0 -> Next 122 | 123 | IAM IDC application 124 | 125 | If you can't find the option of creating a new custom managed application, please [Enable Organizations with IAM Identity Center](https://docs.aws.amazon.com/singlesignon/latest/userguide/get-set-up-for-idc.html). 126 | 127 | - Provide an application name and description and select the below option as shown in the image 128 | 129 | IAM IDC application 130 | 131 | 132 | - Now create a trusted token issuer 133 | 134 | IAM IDC application 135 | 136 | - In the Issuer URL -> provide the ***TrustedIssuerUrl*** from Step 1,provide an issuer name and keep the map attributes as Email 137 | 138 | IAM IDC application 139 | 140 | 141 | - Then navigate back to IAM Identity Center application authentication settings , select the trusted token issuer created in the previous step[refresh it if you don't see in the list] and add the Aud claim -> provide the ***Audience*** from step 1 , then click Next 142 | 143 | IAM IDC application 144 | 145 | - In Specify application credentials , Enter IAM roles -> provide ***RoleArn*** from Step 1 146 | 147 | IAM IDC application 148 | 149 | - Then Review all the steps and create the application. 150 | 151 | - Once the application is created, go to the application and -> Assigned users and groups . 152 | 153 | IAM IDC application 154 | 155 | - Then set up the Trusted application for identity propagation , follow the below steps to Amazon Q as Trusted applications for identity propagation 156 | 157 | IAM IDC application 158 | 159 | IAM IDC application 160 | 161 | IAM IDC application 162 | 163 | Step 4: Once the IAM Identity Center application is created, copy the Application ARN and navigate to Cloudformation to update the previously created Stack. Enter the Identity Center Application ARN in parameter ***IdcApplicationArn*** and run the stack. 164 | 165 | CloudFormation update stack 166 | 167 | Step 5 : Once the update is complete, navigate to Cloudformation output tab to copy the URL and open the URL in a browser 168 | 169 | Step 6 : Streamlit app will prompt to **Connect with Cognito**, For the first login attempt try to Sign up, use the same email id and password for the user that is already exist in IAM Identity Center. 170 | 171 | 172 | ⚡ To eliminate the need for provisioning users in both the Cognito User Pool and the Identity Center, you can follow the link below to create a second custom app (SAML) in the Identity Center. This custom app will act as the Identity Provider for the Cognito User Pool. 173 | 174 | 🔗 [Video](https://www.youtube.com/watch?v=c-hpNhVGnj0&t=522s) 175 | 176 | 🔗 [Instructions](https://repost.aws/knowledge-center/cognito-user-pool-iam-integration) 177 | 178 | 179 | Connect to the EC2 through AWS Session Manager[Optional]: 180 | --------------------------------------------------------------------- 181 | 182 | ``` 183 | sudo -i 184 | cd /opt/custom-web-experience-with-amazon-q-business 185 | ``` 186 | 187 | ## Troubleshooting 188 | 189 | See [TROUBLESHOOTING](TROUBLESHOOTING.md) for more information. 190 | 191 | ## Security 192 | 193 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 194 | 195 | ## License 196 | 197 | This library is licensed under the MIT-0 License. See the LICENSE file. 198 | 199 | 200 | 201 | 202 | -------------------------------------------------------------------------------- /TROUBLESHOOTING.md: -------------------------------------------------------------------------------- 1 | # Common Errors 2 | ## InvalidGrantException: 3 | This error indicates an issue while exchanging the token from the Identity Provider with IAM Identity Center application. 4 | Some sources of error: 5 | * User exists in web app directory (Cognito) but not in IAM Identity Center 6 | * Trusted Token Issuer is misconfigured (attribute mapping or issuer URL). Be careful with trailing slashes as issuer must match with the Identity Provider. You can look at {issuerURL}/.well-known/openid-configuration to confirm the issuer URL. 7 | * Customer Managed Application is misconfigured (audience) 8 | 9 | ## AccessDeniedException when calling CreateTokenWithIAM operation 10 | This error indicates an issue while exchanging the token from the Identity Provider with IAM Identity Center application. 11 | Some sources of error: 12 | * User is not assigned to the customer managed application. You can either assign the users / groups to the customer managed application created or toggle the "Do not require assignments" option. 13 | * The IAM role of the web application is not listed in the Application Credentials of the customer managed application in IAM Identity Center 14 | 15 | ## AccessDeniedException when calling the ChatSync operation 16 | Some sources of error: 17 | * User is not subscribed to the Q application. Hint: Look for `User is not authorized for this service call.` in the error message 18 | 19 | ## Customer follows the blog to replicate the architecture/resoruces, however they observe a certificate error "not secure" in the browser when they try to access the application url. What is the root cause and how to resolve this issue. 20 | 21 | Root Cauase analysis: A public valid certificate is provided from AWS certificate manager, but there is a certificate error showing "not secure" in the browser. The resources creation is followed by the cloud formation template, and the created application url is a load balancer(LB) DNS name. The provided public certificate common name doesnot match load balancer DNS name, that is why there is the certificate error "not secure". 22 | 23 | In order to resolve this issue, please follow the steps below: 24 | 1. Please create a domain record from where you domain service is. Create the domain record which can be validated by your certificate uploaded in LB. For example, if your certificate is xyz.example.com, then you can create a domain record xyz.example.com points to the LB's DNS. If you are using Route53, the record can be A alias record or CName record, if you using other DNS service, it can be CName record. 25 | 26 | 2. Navigate to Amazon Cognito, choose User pools, then choose the user pool created by the cloud formation. Choose the tab App integration, and then scroll down to the bottom of the page to choose the App client. Click the App Client created by the cloud formation. Choose the Hosted UI, and chose Edit. Change the address for "Allowed callback URLs" and replace the LB DNS name to the custom domain name xyz.example.com in our case. The changed "Allowed callback URLs" will be https://xyz.example.com/component/streamlit_oauth.authorize_button/index.html for example. 27 | 28 | 3. Navigate to the AWS App Config service, choose the application created by cloud formation, and visit the Configuration profile details. Copy the profile content to the text editor of your choice. Notice that the value of "ExternalDns" still points to the LB's DNS, and change it to the domain name xyz.example.com. And then click create, and copy paste the content from the text editor and then choose to "create hosted configuraiton version", then click the Start deployment button. This step will make sure that the application will start to use the created custom domain name instead of the DNS LB. 29 | 30 | 4. Test your custom UI by using the custom domain name instead of the LB's DNS, you should observe that the "not secure" error is gone. This is because the created custome domain that the user tries to access is matching the certificate's common name, so the domain name is validated by the certificate. 31 | -------------------------------------------------------------------------------- /docs/Architecture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/custom-web-experience-with-amazon-q-business/46f6917ebdb3e296e6f4e4f35648785bed4603fc/docs/Architecture.jpg -------------------------------------------------------------------------------- /docs/Architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/custom-web-experience-with-amazon-q-business/46f6917ebdb3e296e6f4e4f35648785bed4603fc/docs/Architecture.png -------------------------------------------------------------------------------- /docs/arch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/custom-web-experience-with-amazon-q-business/46f6917ebdb3e296e6f4e4f35648785bed4603fc/docs/arch.jpg -------------------------------------------------------------------------------- /docs/arch_idc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/custom-web-experience-with-amazon-q-business/46f6917ebdb3e296e6f4e4f35648785bed4603fc/docs/arch_idc.png -------------------------------------------------------------------------------- /docs/cfn_update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/custom-web-experience-with-amazon-q-business/46f6917ebdb3e296e6f4e4f35648785bed4603fc/docs/cfn_update.png -------------------------------------------------------------------------------- /docs/iamdic_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/custom-web-experience-with-amazon-q-business/46f6917ebdb3e296e6f4e4f35648785bed4603fc/docs/iamdic_2.png -------------------------------------------------------------------------------- /docs/iamidc_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/custom-web-experience-with-amazon-q-business/46f6917ebdb3e296e6f4e4f35648785bed4603fc/docs/iamidc_3.png -------------------------------------------------------------------------------- /docs/iamidc_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/custom-web-experience-with-amazon-q-business/46f6917ebdb3e296e6f4e4f35648785bed4603fc/docs/iamidc_4.png -------------------------------------------------------------------------------- /docs/iamidcapp_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/custom-web-experience-with-amazon-q-business/46f6917ebdb3e296e6f4e4f35648785bed4603fc/docs/iamidcapp_1.png -------------------------------------------------------------------------------- /docs/iamidcapp_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/custom-web-experience-with-amazon-q-business/46f6917ebdb3e296e6f4e4f35648785bed4603fc/docs/iamidcapp_10.png -------------------------------------------------------------------------------- /docs/iamidcapp_11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/custom-web-experience-with-amazon-q-business/46f6917ebdb3e296e6f4e4f35648785bed4603fc/docs/iamidcapp_11.png -------------------------------------------------------------------------------- /docs/iamidcapp_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/custom-web-experience-with-amazon-q-business/46f6917ebdb3e296e6f4e4f35648785bed4603fc/docs/iamidcapp_5.png -------------------------------------------------------------------------------- /docs/iamidcapp_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/custom-web-experience-with-amazon-q-business/46f6917ebdb3e296e6f4e4f35648785bed4603fc/docs/iamidcapp_6.png -------------------------------------------------------------------------------- /docs/iamidcapp_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/custom-web-experience-with-amazon-q-business/46f6917ebdb3e296e6f4e4f35648785bed4603fc/docs/iamidcapp_7.png -------------------------------------------------------------------------------- /docs/iamidcapp_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/custom-web-experience-with-amazon-q-business/46f6917ebdb3e296e6f4e4f35648785bed4603fc/docs/iamidcapp_8.png -------------------------------------------------------------------------------- /docs/properties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/custom-web-experience-with-amazon-q-business/46f6917ebdb3e296e6f4e4f35648785bed4603fc/docs/properties.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyjwt 2 | streamlit==1.34 3 | streamlit-oauth==0.1.13 4 | streamlit-feedback 5 | boto3 6 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | target-version = "py310" 2 | line-length = 120 3 | [lint] 4 | select = [ 5 | "A", 6 | "ARG", 7 | "B", 8 | "C", 9 | "DTZ", 10 | "E", 11 | "EM", 12 | "F", 13 | "FBT", 14 | "I", 15 | "ICN", 16 | "ISC", 17 | "N", 18 | "PLC", 19 | "PLE", 20 | "PLR", 21 | "PLW", 22 | "Q", 23 | "RUF", 24 | "S", 25 | "T", 26 | "TID", 27 | "UP", 28 | "W", 29 | "YTT", 30 | ] -------------------------------------------------------------------------------- /scripts/self_sign.sh: -------------------------------------------------------------------------------- 1 | openssl req \ 2 | -x509 -nodes -days 365 -sha256 \ 3 | -subj '/C=US/ST=Oregon/L=Portland/CN=sampleexample.com' \ 4 | -newkey rsa:2048 -keyout key.pem -out cert.pem 5 | 6 | aws acm import-certificate --certificate fileb://cert.pem --private-key fileb://key.pem 7 | -------------------------------------------------------------------------------- /src/app.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta, timezone 2 | 3 | import jwt 4 | import jwt.algorithms 5 | import streamlit as st #all streamlit commands will be available through the "st" alias 6 | import utils 7 | from streamlit_feedback import streamlit_feedback 8 | 9 | UTC=timezone.utc 10 | 11 | # Init configuration 12 | utils.retrieve_config_from_agent() 13 | if "aws_credentials" not in st.session_state: 14 | st.session_state.aws_credentials = None 15 | 16 | st.set_page_config(page_title="Amazon Q Business Custom UI") #HTML title 17 | st.title("Amazon Q Business Custom UI") #page title 18 | 19 | # Define a function to clear the chat history 20 | def clear_chat_history(): 21 | st.session_state.messages = [{"role": "assistant", "content": "How may I assist you today?"}] 22 | st.session_state.questions = [] 23 | st.session_state.answers = [] 24 | st.session_state.input = "" 25 | st.session_state["chat_history"] = [] 26 | st.session_state["conversationId"] = "" 27 | st.session_state["parentMessageId"] = "" 28 | 29 | 30 | oauth2 = utils.configure_oauth_component() 31 | if "token" not in st.session_state: 32 | # If not, show authorize button 33 | redirect_uri = f"https://{utils.OAUTH_CONFIG['ExternalDns']}/component/streamlit_oauth.authorize_button/index.html" 34 | result = oauth2.authorize_button("Connect with Cognito",scope="openid", pkce="S256", redirect_uri=redirect_uri) 35 | if result and "token" in result: 36 | # If authorization successful, save token in session state 37 | st.session_state.token = result.get("token") 38 | # Retrieve the Identity Center token 39 | st.session_state["idc_jwt_token"] = utils.get_iam_oidc_token(st.session_state.token["id_token"]) 40 | st.session_state["idc_jwt_token"]["expires_at"] = datetime.now(tz=UTC) + \ 41 | timedelta(seconds=st.session_state["idc_jwt_token"]["expiresIn"]) 42 | st.rerun() 43 | else: 44 | token = st.session_state["token"] 45 | refresh_token = token["refresh_token"] # saving the long lived refresh_token 46 | user_email = jwt.decode(token["id_token"], options={"verify_signature": False})["email"] 47 | if st.button("Refresh Cognito Token") : 48 | # If refresh token button is clicked or the token is expired, refresh the token 49 | token = oauth2.refresh_token(token, force=True) 50 | # Put the refresh token in the session state as it is not returned by Cognito 51 | token["refresh_token"] = refresh_token 52 | # Retrieve the Identity Center token 53 | 54 | st.session_state.token = token 55 | st.rerun() 56 | 57 | if "idc_jwt_token" not in st.session_state: 58 | st.session_state["idc_jwt_token"] = utils.get_iam_oidc_token(token["id_token"]) 59 | st.session_state["idc_jwt_token"]["expires_at"] = datetime.now(UTC) + \ 60 | timedelta(seconds=st.session_state["idc_jwt_token"]["expiresIn"]) 61 | elif st.session_state["idc_jwt_token"]["expires_at"] < datetime.now(UTC): 62 | # If the Identity Center token is expired, refresh the Identity Center token 63 | try: 64 | st.session_state["idc_jwt_token"] = utils.refresh_iam_oidc_token( 65 | st.session_state["idc_jwt_token"]["refreshToken"] 66 | ) 67 | st.session_state["idc_jwt_token"]["expires_at"] = datetime.now(UTC) + \ 68 | timedelta(seconds=st.session_state["idc_jwt_token"]["expiresIn"]) 69 | except Exception as e: 70 | st.error(f"Error refreshing Identity Center token: {e}. Please reload the page.") 71 | 72 | col1, col2 = st.columns([1,1]) 73 | 74 | with col1: 75 | st.write("Welcome: ", user_email) 76 | with col2: 77 | st.button("Clear Chat History", on_click=clear_chat_history) 78 | 79 | # Initialize the chat messages in the session state if it doesn't exist 80 | if "messages" not in st.session_state: 81 | st.session_state["messages"] = [{"role": "assistant", "content": "How can I help you?"}] 82 | 83 | if "conversationId" not in st.session_state: 84 | st.session_state["conversationId"] = "" 85 | 86 | if "parentMessageId" not in st.session_state: 87 | st.session_state["parentMessageId"] = "" 88 | 89 | if "chat_history" not in st.session_state: 90 | st.session_state["chat_history"] = [] 91 | 92 | if "questions" not in st.session_state: 93 | st.session_state.questions = [] 94 | 95 | if "answers" not in st.session_state: 96 | st.session_state.answers = [] 97 | 98 | if "input" not in st.session_state: 99 | st.session_state.input = "" 100 | 101 | 102 | # Display the chat messages 103 | for message in st.session_state.messages: 104 | with st.chat_message(message["role"]): 105 | st.write(message["content"]) 106 | 107 | 108 | # User-provided prompt 109 | if prompt := st.chat_input(): 110 | st.session_state.messages.append({"role": "user", "content": prompt}) 111 | with st.chat_message("user"): 112 | st.write(prompt) 113 | 114 | 115 | # If the last message is from the user, generate a response from the Q_backend 116 | if st.session_state.messages[-1]["role"] != "assistant": 117 | with st.chat_message("assistant"): 118 | with st.spinner("Thinking..."): 119 | placeholder = st.empty() 120 | response = utils.get_queue_chain(prompt,st.session_state["conversationId"], 121 | st.session_state["parentMessageId"], 122 | st.session_state["idc_jwt_token"]["idToken"]) 123 | if "references" in response: 124 | full_response = f"""{response["answer"]}\n\n---\n{response["references"]}""" 125 | else: 126 | full_response = f"""{response["answer"]}\n\n---\nNo sources""" 127 | placeholder.markdown(full_response) 128 | st.session_state["conversationId"] = response["conversationId"] 129 | st.session_state["parentMessageId"] = response["parentMessageId"] 130 | 131 | 132 | st.session_state.messages.append({"role": "assistant", "content": full_response}) 133 | feedback = streamlit_feedback( 134 | feedback_type="thumbs", 135 | optional_text_label="[Optional] Please provide an explanation", 136 | ) 137 | -------------------------------------------------------------------------------- /src/utils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging 3 | import os 4 | 5 | import boto3 6 | import jwt 7 | import streamlit as st 8 | import urllib3 9 | from streamlit_oauth import OAuth2Component 10 | 11 | logger = logging.getLogger() 12 | 13 | # Read the configuration file 14 | APPCONFIG_APP_NAME = os.environ["APPCONFIG_APP_NAME"] 15 | APPCONFIG_ENV_NAME = os.environ["APPCONFIG_ENV_NAME"] 16 | APPCONFIG_CONF_NAME = os.environ["APPCONFIG_CONF_NAME"] 17 | AMAZON_Q_APP_ID = None 18 | IAM_ROLE = None 19 | REGION = None 20 | IDC_APPLICATION_ID = None 21 | OAUTH_CONFIG = {} 22 | 23 | 24 | def retrieve_config_from_agent(): 25 | """ 26 | Retrieve the configuration from the agent 27 | """ 28 | global IAM_ROLE, REGION, IDC_APPLICATION_ID, AMAZON_Q_APP_ID, OAUTH_CONFIG 29 | config = urllib3.request( 30 | "GET", 31 | f"http://localhost:2772/applications/{APPCONFIG_APP_NAME}/environments/{APPCONFIG_ENV_NAME}/configurations/{APPCONFIG_CONF_NAME}", 32 | ).json() 33 | IAM_ROLE = config["IamRoleArn"] 34 | REGION = config["Region"] 35 | IDC_APPLICATION_ID = config["IdcApplicationArn"] 36 | AMAZON_Q_APP_ID = config["AmazonQAppId"] 37 | OAUTH_CONFIG = config["OAuthConfig"] 38 | 39 | 40 | def configure_oauth_component(): 41 | """ 42 | Configure the OAuth2 component for Cognito 43 | """ 44 | cognito_domain = OAUTH_CONFIG["CognitoDomain"] 45 | authorize_url = f"https://{cognito_domain}/oauth2/authorize" 46 | token_url = f"https://{cognito_domain}/oauth2/token" 47 | refresh_token_url = f"https://{cognito_domain}/oauth2/token" 48 | revoke_token_url = f"https://{cognito_domain}/oauth2/revoke" 49 | client_id = OAUTH_CONFIG["ClientId"] 50 | return OAuth2Component( 51 | client_id, None, authorize_url, token_url, refresh_token_url, revoke_token_url 52 | ) 53 | 54 | def refresh_iam_oidc_token(refresh_token): 55 | """ 56 | Refresh the IAM OIDC token using the refresh token retrieved from Cognito 57 | """ 58 | client = boto3.client("sso-oidc", region_name=REGION) 59 | response = client.create_token_with_iam( 60 | clientId=IDC_APPLICATION_ID, 61 | grantType="refresh_token", 62 | refreshToken=refresh_token, 63 | ) 64 | return response 65 | 66 | 67 | def get_iam_oidc_token(id_token): 68 | """ 69 | Get the IAM OIDC token using the ID token retrieved from Cognito 70 | """ 71 | client = boto3.client("sso-oidc", region_name=REGION) 72 | response = client.create_token_with_iam( 73 | clientId=IDC_APPLICATION_ID, 74 | grantType="urn:ietf:params:oauth:grant-type:jwt-bearer", 75 | assertion=id_token, 76 | ) 77 | return response 78 | 79 | 80 | def assume_role_with_token(iam_token): 81 | """ 82 | Assume IAM role with the IAM OIDC idToken 83 | """ 84 | decoded_token = jwt.decode(iam_token, options={"verify_signature": False}) 85 | sts_client = boto3.client("sts", region_name=REGION) 86 | response = sts_client.assume_role( 87 | RoleArn=IAM_ROLE, 88 | RoleSessionName="qapp", 89 | ProvidedContexts=[ 90 | { 91 | "ProviderArn": "arn:aws:iam::aws:contextProvider/IdentityCenter", 92 | "ContextAssertion": decoded_token["sts:identity_context"], 93 | } 94 | ], 95 | ) 96 | st.session_state.aws_credentials = response["Credentials"] 97 | 98 | 99 | # This method create the Q client 100 | def get_qclient(idc_id_token: str): 101 | """ 102 | Create the Q client using the identity-aware AWS Session. 103 | """ 104 | if not st.session_state.aws_credentials: 105 | assume_role_with_token(idc_id_token) 106 | elif st.session_state.aws_credentials["Expiration"] < datetime.datetime.now(datetime.UTC): 107 | assume_role_with_token(idc_id_token) 108 | 109 | session = boto3.Session( 110 | aws_access_key_id=st.session_state.aws_credentials["AccessKeyId"], 111 | aws_secret_access_key=st.session_state.aws_credentials["SecretAccessKey"], 112 | aws_session_token=st.session_state.aws_credentials["SessionToken"], 113 | ) 114 | amazon_q = session.client("qbusiness", REGION) 115 | return amazon_q 116 | 117 | 118 | # This code invoke chat_sync api and format the response for UI 119 | def get_queue_chain( 120 | prompt_input, conversation_id, parent_message_id, token 121 | ): 122 | """" 123 | This method is used to get the answer from the queue chain. 124 | """ 125 | amazon_q = get_qclient(token) 126 | if conversation_id != "": 127 | answer = amazon_q.chat_sync( 128 | applicationId=AMAZON_Q_APP_ID, 129 | userMessage=prompt_input, 130 | conversationId=conversation_id, 131 | parentMessageId=parent_message_id, 132 | ) 133 | else: 134 | answer = amazon_q.chat_sync( 135 | applicationId=AMAZON_Q_APP_ID, userMessage=prompt_input 136 | ) 137 | 138 | system_message = answer.get("systemMessage", "") 139 | conversation_id = answer.get("conversationId", "") 140 | parent_message_id = answer.get("systemMessageId", "") 141 | result = { 142 | "answer": system_message, 143 | "conversationId": conversation_id, 144 | "parentMessageId": parent_message_id, 145 | } 146 | 147 | if answer.get("sourceAttributions"): 148 | attributions = answer["sourceAttributions"] 149 | valid_attributions = [] 150 | 151 | # Generate the answer references extracting citation number, 152 | # the document title, and if present, the document url 153 | for attr in attributions: 154 | title = attr.get("title", "") 155 | url = attr.get("url", "") 156 | citation_number = attr.get("citationNumber", "") 157 | attribution_text = [] 158 | if citation_number: 159 | attribution_text.append(f"[{citation_number}]") 160 | if title: 161 | attribution_text.append(f"Title: {title}") 162 | if url: 163 | attribution_text.append(f", URL: {url}") 164 | 165 | valid_attributions.append("".join(attribution_text)) 166 | 167 | concatenated_attributions = "\n\n".join(valid_attributions) 168 | result["references"] = concatenated_attributions 169 | 170 | # Process the citation numbers and insert them into the system message 171 | citations = {} 172 | for attr in answer["sourceAttributions"]: 173 | for segment in attr["textMessageSegments"]: 174 | citations[segment["endOffset"]] = attr["citationNumber"] 175 | offset_citations = sorted(citations.items(), key=lambda x: x[0]) 176 | modified_message = "" 177 | prev_offset = 0 178 | 179 | for offset, citation_number in offset_citations: 180 | modified_message += ( 181 | system_message[prev_offset:offset] + f"[{citation_number}]" 182 | ) 183 | prev_offset = offset 184 | 185 | modified_message += system_message[prev_offset:] 186 | result["answer"] = modified_message 187 | 188 | return result 189 | --------------------------------------------------------------------------------