# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"). # You may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. --- AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: Amazon Transcribe Live Call Analytics with Agent Assist - LCA AI Stack Parameters: RecordingsBucketRetentionDays: Type: Number Description: "Number of days after which bucket objects will be deleted from the Recordings bucket." Default: 30 CallAudioSource: Type: String AllowedValues: - Demo Asterisk PBX Server -Amazon Chime SDK Voice Connector (SIPREC) - Genesys Cloud Audiohook Web Socket - Amazon Connect Contact Lens Description: > Choose whether to automatically install a demo Asterisk PBX server for easy standalone testing, a Amazon Chime SDK Voice Connector to use for standards based SIPREC/NBR integration with your contact center, or a Web Socket interface to use for Audiohook integration with your Genesys Cloud CX contact center. S3BucketName: Type: String Default: "" Description: > (Optional) Existing bucket where call recording files will be stored. Leave blank to automatically create new bucket. # yamllint disable rule:line-length AllowedPattern: '( *|(?=^.{3,63}$)(?!^(\d+\.)+\d+$)(^(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])$))' # yamllint enable rule:line-length IsSentimentAnalysisEnabled: Type: String Default: 'true' Description: >- Enable sentiment analysis AllowedValues: - 'true' - 'false' SentimentNegativeScoreThreshold: Type: Number Default: 0.9 MinValue: 0 MaxValue: 1 Description: >- Minimum negative sentiment confidence required to declare a phrase as having negative sentiment, in the range 0-1. Not applicable when using Contact Lens or Transcribe Call Analytics (as sentiment is pre-calculated). SentimentPositiveScoreThreshold: Type: Number Default: 0.4 MinValue: 0 MaxValue: 1 Description: >- Minimum positive sentiment confidence required to declare a phrase as having positive sentiment, in the range 0-1. Not applicable when using Contact Lens or Transcribe Call Analytics (as sentiment is pre-calculated). ComprehendLanguageCode: Type: String Description: >- Language code to be used for Amazon Comprehend to detect sentiment. This should match the Amazon Transcribe language. Default: en AllowedValues: - en - es - fr - de - it - pt - ar # - hi - ja - ko - zh # - zh-TW TranscriptLambdaHookFunctionArn: Default: '' Type: String AllowedPattern: '^(|arn:aws:lambda:.*)$' Description: > (Optional) If present, the specified Lambda function is invoked by the LCA Call Event Processor Lambda function for each completed (non-partial) transcript segment. The function can capture and/or modify the text of the transcript, for example to implement custom redaction logic, profanity filtering, or custom rules to highlight patterns in the transcript. TranscriptLambdaHookFunctionNonPartialOnly: Type: String Default: 'true' AllowedValues: - 'true' - 'false' Description: > Specifies if Transcript Lambda Hook Function (if specified) is invoked for Non-Partial transcript segments only (true), or for both Partial and Non-Partial transcript segments (false). EndOfCallTranscriptSummary: Default: 'DISABLED' Type: String AllowedValues: - 'DISABLED' - 'SAGEMAKER' - 'LAMBDA' - 'ANTHROPIC' Description: > Set to enable call summarization by a Large Language Model. The SAGEMAKER option uses a SageMaker endpoint with the pretrained bart-large-cnn-samsum model with a ml.m5.xlarge instance type. The LAMBDA option requires you to provide a function ARN below. The ANTHROPIC option is a third party service, and you must enter your Anthropic API key below. SummarizationSageMakerInitialInstanceCount: Type: Number MinValue: 0 Default: 1 Description: > (Optional) If "End Of Call Transcript Summary" is SAGEMAKER, provide initial instance count. Set to '0' to enable Serverless Inference (for cold-start delay tolerant deployments only). SummarizationLLMThirdPartyApiKey: Type: String Description: 'Optional: If EndOfCallTranscriptSummary is ANTHROPIC, enter the provider API Key. ** Data will leave your AWS account **' Default: '' NoEcho: true EndOfCallLambdaHookFunctionArn: Default: '' Type: String AllowedPattern: '^(|arn:aws:lambda:.*)$' Description: > (Optional) If 'End Of Call Transcript Summary' is LAMBDA, provide ARN for a Lambda function. The specified Lambda function is invoked by the LCA Call Event Processor Lambda function for end of call event. The function is passed en event with CallId as input. This function can implement custom logic that is relevant to end of call processing, for example, creating a call summary. StartOfCallLambdaHookFunctionArn: Default: '' Type: String AllowedPattern: '^(|arn:aws:lambda:.*)$' Description: > (Optional) The specified Lambda function is invoked by the LCA Call Event Processor Lambda function for beginning or start of call event. This function can implement custom logic that is relevant to beginning of call processing, for example, retrieving call summary details logged into a case in a CRM. PostCallSummaryLambdaHookFunctionArn: Default: '' Type: String AllowedPattern: '^(|arn:aws:lambda:.*)$' Description: > (Optional) The specified Lambda function is invoked by the LCA Call Event Processor Lambda function after the call summary is processed. This function can implement custom logic that is relevant to post processing, for example, updating the call summary to a CRM system. CloudFrontPriceClass: Type: String Default: PriceClass_100 Description: >- Specify the CloudFront price class. See https://aws.amazon.com/cloudfront/pricing/ for a description of each price class. AllowedValues: - PriceClass_100 - PriceClass_200 - PriceClass_All ConstraintDescription: >- Allowed Price Classes PriceClass_100 PriceClass_200 and PriceClass_All CloudFrontAllowedGeos: Type: String Default: '' Description: >- Specify a comma separated list of two letter country codes (uppercase ISO 3166-1) that are allowed to access the web user interface via CloudFront. For example: US,CA. Leave empty if you do not want geo restrictions to be applied. AllowedPattern: '^(|[A-Z]{2}(,[A-Z]{2})*)$' ConstraintDescription: >- Comma separated list of uppercase two letter country codes or empty AdminEmail: Type: String Description: >- Email address of admin user (e.g. jdoe@example.com) used for the API and web UI. An initial temporary password will be automatically sent to this user via email. AllowedPattern: '^[\w.+-]+@([\w-]+\.)+[\w-]{2,6}$' AllowedSignUpEmailDomain: Type: String Default: '' Description: >- Email address domain (example.com) or comma separated list of email domains (example1.com, example2.com) allowed to signin and signup using the web UI. If left empty, signup via the web UI is disabled and users will have to be created using Cognito. AllowedPattern: '^(|([\w-]+\.)+[\w-]{2,6}(, *([\w-]+\.)+[\w-]{2,6})*)$' IsLexAgentAssistEnabled: Type: String Default: 'false' Description: >- Enables the Lex Agent Assist feature. AllowedValues: - 'true' - 'false' IsLambdaAgentAssistEnabled: Type: String Default: 'false' Description: >- Enables the Lambda Agent Assist feature. The AgentAssistExistingLambdaFunctionArn parameters should have values if this is set to 'true' AllowedValues: - 'true' - 'false' AgentAssistExistingLambdaFunctionArn: Default: '' Type: String AllowedPattern: '^(|arn:aws:lambda:.*)$' Description: > Used only if IsLambdaAgentAssistEnabled is set to "true". Provide the function ARN of an existing Lambda function to be used for Agent Assist." DynamoDbExpirationInDays: Type: Number Default: 90 Description: >- Number of days set in the time to live of event data stored in the DynamoDB table # NOTE: These parameters are dynamically updated during release BootstrapBucketBaseName: Type: String Default: aws-bigdata-blog Description: >- Base name of bootstrap S3 bucket. The region is appended to the bucket name. For example if you provide a base name `mybucket`, a bucket with a region suffix must exist in the region you are deploying (e.g. `mybucket-us-east-1`) The bucket contains pre-staged packaged templates and source artifacts # yamllint disable rule:line-length AllowedPattern: '(?=^.{3,63}$)(?!^(\d+\.)+\d+$)(^(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])$)' # yamllint enable rule:line-length BootstrapS3Prefix: Type: String Default: artifacts/lca Description: > S3 prefix where the templates and source are stored under BootstrapVersion: Type: String Default: 0.8.3 Description: > Artifacts version (semver). Used to point to a specific release in the S3 bootstrap bucket CategoryAlertRegEx: Type: String Default: .* Description: > A regular expression that will be used to show an alert in the UI if a category matches EnableVoiceToneAnalysis: Type: String Default: 'false' AllowedValues: - 'true' - 'false' Description: > Whether or not to show the voice tone analysis panel in the UI Metadata: "AWS::CloudFormation::Interface": ParameterGroups: - Label: default: Amazon S3 Configuration Parameters: - S3BucketName - Label: default: Amazon CloudFront Configuration Parameters: - CloudFrontPriceClass - CloudFrontAllowedGeos ParameterLabels: S3BucketName: default: Call Audio Bucket Name CloudFrontPriceClass: default: CloudFront Price Class CloudFrontAllowedGeos: default: CloudFront Allowed Geographies IsSentimentAnalysisEnabled: default: Enable Sentiment Analysis ComprehendLanguageCode: default: Comprehend Sentiment Analysis Language Code TranscriptLambdaHookFunctionArn: default: Lambda Hook Function ARN for Custom Transcript Segment Processing (existing) TranscriptLambdaHookFunctionNonPartialOnly: default: Lambda Hook Function Mode Non-Partial only SummarizationSageMakerInitialInstanceCount: default: Initial Instance Count for Summarization SageMaker Endpoint EndOfCallLambdaHookFunctionArn: default: Lambda Hook Function ARN for Custom End of Call Processing (existing) EndOfCallTranscriptSummary: default: End of Call Transcript Summary StartOfCallLambdaHookFunctionArn: default: Lambda Hook Function ARN for Custom Start of Call Processing (existing) PostCallSummaryLambdaHookFunctionArn: default: Lambda Hook Function ARN for Custom Post Processing, after the Call Transcript Summary is processed (existing) Conditions: ShouldCreateRecordingBucket: !Equals [!Ref S3BucketName, ""] ShouldAllowSignUpEmailDomain: !Not [!Equals [!Ref AllowedSignUpEmailDomain, ""]] ShouldEnableGeoRestriction: !Not [!Equals [!Ref CloudFrontAllowedGeos, '']] ShouldEnableLambdaAgentAssist: !Equals [!Ref IsLambdaAgentAssistEnabled, "true"] ShouldEnableTranscriptLambdaHookFunction: !Not [!Equals [!Ref TranscriptLambdaHookFunctionArn, ""]] ShouldEnableStartOfCallLambdaHookFunction: !Not [!Equals [!Ref StartOfCallLambdaHookFunctionArn, ""]] ShouldEnablePostCallSummaryLambdaHookFunction: !Not [!Equals [!Ref PostCallSummaryLambdaHookFunctionArn, ""]] ShouldEnableAnthropicSummarizer: !Equals [!Ref EndOfCallTranscriptSummary, "ANTHROPIC"] ShouldEnableEndOfCallLambdaHookFunction: !Equals [!Ref EndOfCallTranscriptSummary, "LAMBDA"] ShouldDeploySageMakerSummarizer: !Equals [!Ref EndOfCallTranscriptSummary, "SAGEMAKER"] IsTranscriptSummaryEnabled: !Or - !Condition ShouldEnableEndOfCallLambdaHookFunction - !Condition ShouldDeploySageMakerSummarizer - !Condition ShouldEnableAnthropicSummarizer Outputs: CallDataStreamName: Description: >- The Name of Kinesis Data Stream to write the call data to. Value: !Ref CallDataStream CallDataStreamArn: Description: >- The ARN of Kinesis Data Stream to write the call data to. Value: !GetAtt CallDataStream.Arn S3BucketName: Description: Bucket which contains all the call recordings Value: !If - ShouldCreateRecordingBucket - !Ref RecordingsBucket - !Ref S3BucketName CloudfrontEndpoint: Description: Endpoint for Cloudfront distribution Value: !Sub "https://${WebAppCloudFrontDistribution.DomainName}/" EventSourcingTableName: Description: The Name for the DynamoDB that is used to store transcriptions and events Value: !Ref EventSourcingTable EventSourcingTableArn: Description: >- The ARN of the DynamoDB table created to store events and the contact details used in this solution Value: !GetAtt EventSourcingTable.Arn CallEventProcessorFunctionRoleName: Description: The Name of the IAM role used by CallEventProcessorFunction Value: !Ref CallEventProcessorFunctionRole SNSTopic: Description: The SNS topic name that LCA publishes category events to Value: !GetAtt CategorySNSTopic.TopicName FetchTranscriptArn: Description: The arn for a Lambda function that fetches a call transcript as a string Value: !GetAtt FetchTranscript.Arn LexAgentAssistIdentityPoolId: Description: The Id for the Cognito Identity Pool created for the agent assist Value: !Ref AgentAssistBotIdentityPool LexAgentAssistUnauthRoleArn: Description: The arn for the Agent Assist Bot Identity Pool Cognito role Value: !GetAtt AgentAssistBotUnauthRole.Arn CloudFrontDistributionId: Description: The ID of the CloudFront distribution for LCA's main web ui Value: !Ref WebAppCloudFrontDistribution WebAppBucket: Description: Name of S3 web app bucket Value: !Ref WebAppBucket CloudFrontDomainName: Description: The full domain name of the CloudFront distribution Value: !GetAtt WebAppCloudFrontDistribution.DomainName Resources: ## BUCKET TO STORE STEREO RECORDINGS RecordingsBucket: Type: AWS::S3::Bucket DeletionPolicy: Retain UpdateReplacePolicy: Retain Condition: ShouldCreateRecordingBucket Properties: AccessControl: LogDeliveryWrite OwnershipControls: Rules: - ObjectOwnership: ObjectWriter # XXX figure out access logging # LoggingConfiguration: # DestinationBucketName: !Ref S3BucketName # LogFilePrefix: 'logs/' VersioningConfiguration: Status: Enabled PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 LifecycleConfiguration: Rules: - Id: RuleRetention Status: Enabled Prefix: "" ExpirationInDays: !Ref RecordingsBucketRetentionDays RecordingsBucketPolicy: Type: AWS::S3::BucketPolicy Condition: ShouldCreateRecordingBucket Properties: Bucket: !Ref RecordingsBucket PolicyDocument: Version: 2012-10-17 Statement: - Action: - "s3:*" Effect: "Deny" Principal: "*" Resource: - !GetAtt RecordingsBucket.Arn - !Sub "${RecordingsBucket.Arn}/*" Condition: Bool: "aws:SecureTransport": false EventSourcingTable: Type: AWS::DynamoDB::Table DeletionPolicy: Retain UpdateReplacePolicy: Retain Properties: AttributeDefinitions: # primary key attributes - AttributeName: PK AttributeType: S - AttributeName: SK AttributeType: S KeySchema: - AttributeName: PK KeyType: HASH - AttributeName: SK KeyType: RANGE BillingMode: PAY_PER_REQUEST PointInTimeRecoverySpecification: PointInTimeRecoveryEnabled: true SSESpecification: SSEEnabled: true TimeToLiveSpecification: AttributeName: ExpiresAfter Enabled: true StreamSpecification: StreamViewType: NEW_IMAGE # Add permissions to the default key to allow S3 access SQSManagedKey: Type: AWS::KMS::Key Properties: EnableKeyRotation: true KeyPolicy: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: - "s3.amazonaws.com" Action: - "kms:GenerateDataKey*" - "kms:Decrypt" Resource: "*" - Effect: Allow Principal: AWS: !Join - '' - - 'arn:aws:iam::' - !Ref 'AWS::AccountId' - ':root' Action: - "kms:*" Resource: "*" ########################################################################## # ParameterStore ########################################################################## LCASettingsParameter: Type: AWS::SSM::Parameter Properties: Type: String Value: !Sub '{ "CategoryAlertRegex":"${CategoryAlertRegEx}", "EnableVoiceToneAnalysis":"${EnableVoiceToneAnalysis}" }' ########################################################################## # CodeBuild ########################################################################## TranscriberCodeBuildServiceRole: Type: AWS::IAM::Role Properties: Path: / AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Action: sts:AssumeRole Effect: Allow Principal: Service: codebuild.amazonaws.com Policies: - PolicyName: ecs-service PolicyDocument: Version: 2012-10-17 Statement: - Resource: - !Sub "arn:aws:s3:::${BootstrapBucketBaseName}-${AWS::Region}" - !Sub "arn:aws:s3:::${BootstrapBucketBaseName}-${AWS::Region}/\ ${BootstrapS3Prefix}/*" Effect: Allow Action: - s3:GetObject - s3:GetObjectVersion - s3:GetBucketAcl - s3:GetBucketLocation - s3:PutObject - s3:ListBucket - Resource: - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/*" - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:\ /aws/codebuild/*:*" Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents - Resource: "*" Effect: Allow Action: - ecr:GetAuthorizationToken - Effect: Allow Action: - s3:PutObject - s3:DeleteObject Resource: - !Sub "arn:aws:s3:::${WebAppBucket}/*" - Effect: Allow Action: - cloudfront:CreateInvalidation Resource: - !Sub "arn:${AWS::Partition}:cloudfront::${AWS::AccountId}:\ distribution/${WebAppCloudFrontDistribution}" Metadata: cfn_nag: rules_to_suppress: - id: W11 reason: >- ECR do not support resource-level permissions for GetAuthorizationToken and therefore cannot be specificed directly. TranscriberCodeBuildProject: Type: AWS::CodeBuild::Project Properties: Description: !Sub >- Builds docker images and the website of stack: ${AWS::StackName} ServiceRole: !Ref TranscriberCodeBuildServiceRole EncryptionKey: alias/aws/s3 Artifacts: Type: NO_ARTIFACTS Source: Location: !Sub "arn:aws:s3:::${BootstrapBucketBaseName}-${AWS::Region}/${BootstrapS3Prefix}/\ ${BootstrapVersion}/src.zip" Type: S3 BuildSpec: | version: 0.2 phases: pre_build: commands: - echo ${SOURCE_CODE_LOCATION} - echo `ls -altr` - echo `pwd` - echo Installing NodeJS - n 14.19.0 - npm install -g npm@8.4.1 - echo Installing Web UI dependencies - cd source/ui - npm install build: commands: - echo Build started on `date` - echo Building kvs transcribe streamig container image - cd $CODEBUILD_SRC_DIR - cd source/ui - echo Building Web UI - npm run build - > printf '{"RepositoryUri":"%s","ProjectName":"%s","ArtifactBucket":"%s"}' $REPOSITORY_URI $PROJECT_NAME $ARTIFACT_BUCKET > build.json post_build: commands: - echo Build completed on `date` - echo Copying Web UI - find build -ls - aws s3 cp --recursive build s3://${WEBAPP_BUCKET}/ - echo Invalidating CloudFront Distribution - > aws cloudfront create-invalidation --distribution-id "$CLOUDFRONT_DISTRIBUTION_ID" --paths '/*' artifacts: files: - build.json Environment: ComputeType: BUILD_GENERAL1_MEDIUM Image: aws/codebuild/amazonlinux2-x86_64-standard:4.0 Type: LINUX_CONTAINER PrivilegedMode: True EnvironmentVariables: - Name: AWS_DEFAULT_REGION Value: !Ref AWS::Region - Name: AWS_ACCOUNT_ID Value: !Ref AWS::AccountId - Name: IMAGE_TAG Value: !Ref BootstrapVersion - Name: SOURCE_CODE_LOCATION Value: !Sub "${BootstrapBucketBaseName}-${AWS::Region}/\ ${BootstrapS3Prefix}/${BootstrapVersion}" - Name: WEBAPP_BUCKET Value: !Ref WebAppBucket - Name: CLOUDFRONT_DISTRIBUTION_ID Value: !Ref WebAppCloudFrontDistribution # These REACT_APP_ variables are used by the web ui in the # aws-exports.js Amplify config. The values are embedded in the # code at build time. See: # https://create-react-app.dev/docs/adding-custom-environment-variables/ - Name: REACT_APP_SETTINGS_PARAMETER Value: !Ref LCASettingsParameter - Name: REACT_APP_USER_POOL_ID Value: !Ref UserPool - Name: REACT_APP_USER_POOL_CLIENT_ID Value: !Ref UserPoolClient - Name: REACT_APP_IDENTITY_POOL_ID Value: !Ref IdentityPool - Name: REACT_APP_APPSYNC_GRAPHQL_URL Value: !GetAtt AppSyncApi.GraphQLUrl - Name: REACT_APP_AWS_REGION Value: !Ref AWS::Region - Name: REACT_APP_SHOULD_HIDE_SIGN_UP Value: !If - ShouldAllowSignUpEmailDomain - 'false' - 'true' - Name: REACT_APP_ENABLE_LEX_AGENT_ASSIST Value: !Ref IsLexAgentAssistEnabled - Name: REACT_APP_CLOUDFRONT_DOMAIN Value: !Sub "https://${WebAppCloudFrontDistribution.DomainName}/" TimeoutInMinutes: 10 LambdaCodeBuildStartBuildExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: "/" Policies: - PolicyName: root PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - codebuild:StartBuild - codebuild:BatchGetBuilds Resource: !GetAtt TranscriberCodeBuildProject.Arn - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: - !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:\ /aws/lambda/*" # Used by custom resource helper poller # https://github.com/aws-cloudformation/custom-resource-helper - PolicyName: CustomResourcePoller PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - events:PutRule - events:DeleteRule - events:PutTargets - events:RemoveTargets Resource: - !Sub "arn:${AWS::Partition}:events:${AWS::Region}:\ ${AWS::AccountId}:rule/*" - Effect: Allow Action: - lambda:AddPermission - lambda:RemovePermission Resource: - !Sub "arn:${AWS::Partition}:lambda:${AWS::Region}:\ ${AWS::AccountId}:function:*" CodeBuildRun: Type: Custom::CodeBuildRun Properties: ServiceToken: !GetAtt LambdaCodeBuildStartBuild.Arn BuildProjectName: !Ref TranscriberCodeBuildProject IsLexAgentAssistEnabled: !Ref IsLexAgentAssistEnabled EnableVoiceToneAnalysis: !Ref EnableVoiceToneAnalysis # pass code location to support upgrades CodeLocation: !Sub "arn:aws:s3:::${BootstrapBucketBaseName}-${AWS::Region}/${BootstrapS3Prefix}/\ ${BootstrapVersion}/src.zip" LambdaCodeBuildStartBuild: Type: AWS::Serverless::Function Properties: Role: !GetAtt LambdaCodeBuildStartBuildExecutionRole.Arn Runtime: python3.9 Timeout: 60 MemorySize: 128 Handler: lambda_start_codebuild.handler CodeUri: ../source/lambda_functions/start_codebuild Description: This AWS Lambda Function kicks off a code build job. Metadata: cfn_nag: rules_to_suppress: - id: W89 reason: Customer can use VPC if desired ########################################################################## # Kinesis Data Stream ########################################################################## CallDataStream: Type: AWS::Kinesis::Stream Properties: # TODO pass retention from a parameter from 24hrs (1day) up to 8760hrs (365days) RetentionPeriodHours: 24 StreamModeDetails: StreamMode: ON_DEMAND StreamEncryption: EncryptionType: KMS KeyId: alias/aws/kinesis DataStreamConsumer: Type: AWS::Kinesis::StreamConsumer Properties: StreamARN: !GetAtt CallDataStream.Arn ConsumerName: DataStreamConsumer ########################################################################## # Transcript Summary ########################################################################## SageMakerTranscriptSummaryStack: Type: AWS::CloudFormation::Stack Condition: ShouldDeploySageMakerSummarizer Properties: # yamllint disable rule:line-length TemplateURL: !Sub https://s3.${AWS::Region}.amazonaws.com/${BootstrapBucketBaseName}-${AWS::Region}/${BootstrapS3Prefix}/${BootstrapVersion}/sagemaker-summary-stack.yaml # yamllint enable rule:line-length Parameters: CallEventsTable: !Ref EventSourcingTable InitialInstanceCount: !Ref SummarizationSageMakerInitialInstanceCount FetchTranscriptArn: !GetAtt FetchTranscript.Arn FetchTranscript: Type: AWS::Serverless::Function Properties: Role: !GetAtt LambdaFetchTranscriptExecutionRole.Arn Runtime: python3.9 Environment: Variables: LCA_CALL_EVENTS_TABLE: !Ref EventSourcingTable Timeout: 60 MemorySize: 128 Handler: index.lambda_handler CodeUri: ../source/lambda_functions/fetch_transcript Description: This AWS Lambda Function fetches the call transcript for processing. Metadata: cfn_nag: rules_to_suppress: - id: W89 reason: Customer can use VPC if desired LambdaFetchTranscriptExecutionRole: Type: AWS::IAM::Role Properties: Description: Fetch Transcript Lambda Role AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: - 'sts:AssumeRole' ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Policies: - PolicyName: !Sub ${AWS::StackName}-FetchTranscript PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: "dynamodb:Query" Resource: !GetAtt EventSourcingTable.Arn LLMAnthropicSummaryLambda: Type: AWS::Serverless::Function Properties: Role: !GetAtt LLMAnthropicSummaryLambdaExecutionRole.Arn Runtime: python3.9 Environment: Variables: LCA_CALL_EVENTS_TABLE: !Ref EventSourcingTable ANTHROPIC_MODEL_IDENTIFIER: "claude-v1.3-100k" ANTHROPIC_API_KEY: !Ref SummarizationLLMThirdPartyApiKey ENDPOINT_URL: "https://api.anthropic.com/v1/complete" FETCH_TRANSCRIPT_LAMBDA_ARN: !GetAtt FetchTranscript.Arn PROCESS_TRANSCRIPT: "True" TOKEN_COUNT: "0" # using 100k model - do not truncate transcript SUMMARY_PROMPT_TEMPLATE: "

Human:
{transcript}

Summarize the above transcript in no more than 5 sentences, using gender neutral pronouns. Were the caller's needs met during the call?

Assistant: Here is a summary in 5 sentences:" Timeout: 60 MemorySize: 128 Handler: index.handler CodeUri: ../source/lambda_functions/llm_anthropic_summary_lambda Description: This AWS Lambda Function runs a transcript summary inference on the call transcript. Metadata: cfn_nag: rules_to_suppress: - id: W89 reason: Customer can use VPC if desired LLMAnthropicSummaryLambdaExecutionRole: Type: AWS::IAM::Role Properties: Description: LLM Anthropic Summary Lambda Role AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: - 'sts:AssumeRole' ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Policies: - PolicyName: !Sub ${AWS::StackName}-AnthropicSummary PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: "lambda:InvokeFunction" Resource: !GetAtt FetchTranscript.Arn AsyncAgentAssistOrchestrator: Type: AWS::Serverless::Function Properties: Role: !GetAtt AsyncAgentAssistOrchestratorExecutionRole.Arn Runtime: python3.9 Environment: Variables: CALL_DATA_STREAM_NAME: !Ref CallDataStream LEX_BOT_ID: 'Pending AgentAssistSetup' LEX_BOT_ALIAS_ID: 'Pending AgentAssistSetup' LEX_BOT_LOCALE_ID: 'Pending AgentAssistSetup' # Lex Agent Assist IS_LEX_AGENT_ASSIST_ENABLED: !Ref IsLexAgentAssistEnabled # Lambda Agent Assist IS_LAMBDA_AGENT_ASSIST_ENABLED: !Ref IsLambdaAgentAssistEnabled LAMBDA_AGENT_ASSIST_FUNCTION_ARN: !Ref AgentAssistExistingLambdaFunctionArn DYNAMODB_TABLE_NAME: !Ref EventSourcingTable Timeout: 60 MemorySize: 128 Handler: lambda_function.handler Layers: - !Ref TranscriptEnrichmentPythonLayer CodeUri: ../source/lambda_functions/async_agent_assist_orchestrator Description: This AWS Lambda Function orchestrates agent assist processing. Metadata: cfn_nag: rules_to_suppress: - id: W89 reason: Customer can use VPC if desired AsyncAgentAssistOrchestratorExecutionRole: Type: AWS::IAM::Role Properties: Description: Async Agent Assist Orchestrator Lambda Role AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: - 'sts:AssumeRole' ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Policies: - PolicyName: !Sub ${AWS::StackName}-AsyncAgentAssist PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: kinesis:PutRecord Resource: !GetAtt CallDataStream.Arn - Effect: Allow Action: - lex:RecognizeText Resource: # XXX make specific to bot passed in parameter "*" - !If - ShouldEnableLambdaAgentAssist - Effect: Allow Action: - lambda:InvokeFunction Resource: - !Sub "${AgentAssistExistingLambdaFunctionArn}" - Ref: AWS::NoValue AsyncTranscriptSummaryOrchestrator: Type: AWS::Serverless::Function Properties: Role: !GetAtt AsyncTranscriptSummaryOrchestratorExecutionRole.Arn Runtime: python3.9 Environment: Variables: TRANSCRIPT_SUMMARY_FUNCTION_ARN: !If [ ShouldDeploySageMakerSummarizer, !GetAtt [SageMakerTranscriptSummaryStack, Outputs.InvokeLambdaArn], !If [ ShouldEnableAnthropicSummarizer, !GetAtt LLMAnthropicSummaryLambda.Arn, !Ref EndOfCallLambdaHookFunctionArn ] ] CALL_DATA_STREAM_NAME: !Ref CallDataStream Timeout: 60 MemorySize: 128 Handler: lambda_function.handler Layers: - !Ref TranscriptEnrichmentPythonLayer CodeUri: ../source/lambda_functions/async_transcript_summary_orchestrator Description: This AWS Lambda Function orchestrates call summary processing processing. Metadata: cfn_nag: rules_to_suppress: - id: W89 reason: Customer can use VPC if desired AsyncTranscriptSummaryOrchestratorExecutionRole: Type: AWS::IAM::Role Properties: Description: Async Transcript Summary Orchetrator Lambda Role AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: - 'sts:AssumeRole' ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Policies: - PolicyName: !Sub ${AWS::StackName}-AsyncTranscriptSummary PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: kinesis:PutRecord Resource: !GetAtt CallDataStream.Arn - !If - ShouldEnableEndOfCallLambdaHookFunction - Effect: Allow Action: - lambda:InvokeFunction Resource: !Sub "${EndOfCallLambdaHookFunctionArn}" - Ref: AWS::NoValue - !If - ShouldDeploySageMakerSummarizer - Effect: Allow Action: - lambda:InvokeFunction Resource: - !GetAtt SageMakerTranscriptSummaryStack.Outputs.InvokeLambdaArn - Ref: AWS::NoValue - !If - ShouldEnableAnthropicSummarizer - Effect: Allow Action: - lambda:InvokeFunction Resource: - !GetAtt LLMAnthropicSummaryLambda.Arn - Ref: AWS::NoValue ########################################################################## # LexWebUI / Agent Assist Bot ########################################################################## AgentAssistBotIdentityPool: Type: AWS::Cognito::IdentityPool Properties: IdentityPoolName: !Sub "${AWS::StackName}-AgentAssistBot-IdentityPool" AllowUnauthenticatedIdentities: true AgentAssistBotIdentityPoolSetRole: Type: AWS::Cognito::IdentityPoolRoleAttachment Properties: IdentityPoolId: !Ref AgentAssistBotIdentityPool Roles: unauthenticated: !GetAtt AgentAssistBotUnauthRole.Arn AgentAssistBotUnauthRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Federated: cognito-identity.amazonaws.com Action: - sts:AssumeRoleWithWebIdentity Condition: StringEquals: "cognito-identity.amazonaws.com:aud": !Ref AgentAssistBotIdentityPool "ForAnyValue:StringLike": "cognito-identity.amazonaws.com:amr": unauthenticated Policies: - PolicyName: AgentAssistBotUnauthPolicy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - lex:RecognizeText Resource: !Sub "arn:${AWS::Partition}:lex:${AWS::Region}:${AWS::AccountId}:bot-alias/PLCHLDR/PLCHLDR" ########################################################################## # SNS Topic for Category Event Notifications ########################################################################## CategorySNSTopic: Type: AWS::SNS::Topic ########################################################################## # Call Event Processor ########################################################################## CallEventProcessorFunction: Type: AWS::Serverless::Function Metadata: cfn_nag: rules_to_suppress: - id: W89 reason: Customer can use VPC if desired Properties: Architectures: - arm64 CodeUri: ../source/lambda_functions/call_event_processor Description: !Sub >- Call Event and Transcript Processor for stack: ${AWS::StackName} Events: TranscriptKds: Type: Kinesis Properties: # https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-kinesis.html#sam-property-function-kinesis--examples BatchSize: 200 BisectBatchOnFunctionError: true # TODO Add SQS queue for discarded records # DestinationConfig: Enabled: true MaximumRetryAttempts: 2 MaximumBatchingWindowInSeconds: 0 ParallelizationFactor: 10 # XXX MaximumRecordAgeInSeconds: 900 StartingPosition: LATEST Stream: !GetAtt DataStreamConsumer.ConsumerARN # Tumbling Window is used for aggregations over different invocations # https://docs.aws.amazon.com/lambda/latest/dg/with-kinesis.html#services-kinesis-windows TumblingWindowInSeconds: 0 Handler: lambda_function.handler Layers: - !Ref TranscriptEnrichmentPythonLayer MemorySize: 3000 Policies: - DynamoDBCrudPolicy: TableName: !Ref EventSourcingTable - Statement: - Effect: Allow Action: - sns:Publish Resource: !Ref CategorySNSTopic - Effect: Allow Action: - ssm:GetParameter Resource: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${LCASettingsParameter}" - Effect: Allow Action: - appsync:GraphQL Resource: # XXX specific mutation - addEnrichment - !Sub "${AppSyncApi.Arn}/types/Mutation/*" - !Sub "${AppSyncApi.Arn}/types/Query/fields/__schema" - !Sub "${AppSyncApi.Arn}/types/Query/fields/getCall" - !Sub "${AppSyncApi.Arn}/types/Query/*" - Effect: Allow Action: - comprehend:DetectSentiment Resource: "*" - !If - ShouldEnableTranscriptLambdaHookFunction - Effect: Allow Action: - lambda:InvokeFunction Resource: !Sub "${TranscriptLambdaHookFunctionArn}" - Ref: AWS::NoValue - !If - ShouldEnableStartOfCallLambdaHookFunction - Effect: Allow Action: - lambda:InvokeFunction Resource: !Sub "${StartOfCallLambdaHookFunctionArn}" - Ref: AWS::NoValue - !If - ShouldEnablePostCallSummaryLambdaHookFunction - Effect: Allow Action: - lambda:InvokeFunction Resource: !Sub "${PostCallSummaryLambdaHookFunctionArn}" - Ref: AWS::NoValue - !If - IsTranscriptSummaryEnabled - Effect: Allow Action: - lambda:InvokeFunction Resource: !GetAtt AsyncTranscriptSummaryOrchestrator.Arn - Ref: AWS::NoValue - Effect: Allow Action: - lambda:InvokeFunction Resource: !GetAtt AsyncAgentAssistOrchestrator.Arn Runtime: python3.9 Timeout: 60 Environment: Variables: # XXX add log level parameter or map LOG_LEVEL: DEBUG POWERTOOLS_METRICS_NAMESPACE: !Sub "TranscriptProcessor-${AWS::StackName}" POWERTOOLS_SERVICE_NAME: TranscriptProcessor # XXX add trace parameter or map POWERTOOLS_TRACE_DISABLED: false APPSYNC_GRAPHQL_URL: !GetAtt AppSyncApi.GraphQLUrl STATE_DYNAMODB_TABLE_NAME: !Ref EventSourcingTable COMPREHEND_LANGUAGE_CODE: !Ref ComprehendLanguageCode IS_SENTIMENT_ANALYSIS_ENABLED: !Ref IsSentimentAnalysisEnabled SENTIMENT_NEGATIVE_THRESHOLD: !Ref SentimentNegativeScoreThreshold SENTIMENT_POSITIVE_THRESHOLD: !Ref SentimentPositiveScoreThreshold CALL_AUDIO_SOURCE: !Ref CallAudioSource # Lex Agent Assist IS_LEX_AGENT_ASSIST_ENABLED: !Ref IsLexAgentAssistEnabled # Lambda Agent Assist IS_LAMBDA_AGENT_ASSIST_ENABLED: !Ref IsLambdaAgentAssistEnabled DYNAMODB_EXPIRATION_IN_DAYS: !Ref DynamoDbExpirationInDays TRANSCRIPT_LAMBDA_HOOK_FUNCTION_ARN: !Ref TranscriptLambdaHookFunctionArn START_OF_CALL_LAMBDA_HOOK_FUNCTION_ARN: !Ref StartOfCallLambdaHookFunctionArn POST_CALL_SUMMARY_LAMBDA_HOOK_FUNCTION_ARN: !Ref PostCallSummaryLambdaHookFunctionArn TRANSCRIPT_LAMBDA_HOOK_FUNCTION_NONPARTIAL_ONLY: !Ref TranscriptLambdaHookFunctionNonPartialOnly IS_TRANSCRIPT_SUMMARY_ENABLED: !If [IsTranscriptSummaryEnabled, "true", "false"] ASYNC_TRANSCRIPT_SUMMARY_ORCHESTRATOR_ARN: !GetAtt AsyncTranscriptSummaryOrchestrator.Arn ASYNC_AGENT_ASSIST_ORCHESTRATOR_ARN: !GetAtt AsyncAgentAssistOrchestrator.Arn CALL_DATA_STREAM_NAME: !Ref CallDataStream SNS_TOPIC_ARN: !Ref CategorySNSTopic PARAMETER_STORE_NAME: !Ref LCASettingsParameter ########################################################################## # Transcript Enrichment Lambda Layers ########################################################################## TranscriptEnrichmentPythonLayer: Type: AWS::Serverless::LayerVersion Metadata: BuildMethod: python3.9 Properties: Description: !Sub "Transcript Enrichment Python Layer for stack: ${AWS::StackName}" ContentUri: ../source/lambda_layers/transcript_enrichment_layer CompatibleArchitectures: - arm64 CompatibleRuntimes: - python3.9 RetentionPolicy: Delete ########################################################################## # AppSync ########################################################################## AppSyncCwlRole: Type: AWS::IAM::Role Properties: ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSAppSyncPushToCloudWatchLogs AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - sts:AssumeRole Principal: Service: - appsync.amazonaws.com AppSyncDynamoDbRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - sts:AssumeRole Principal: Service: - appsync.amazonaws.com Policies: - PolicyName: dynamoDB PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:Query - dynamodb:Scan - dynamodb:GetItem - dynamodb:PutItem - dynamodb:UpdateItem Resource: !GetAtt EventSourcingTable.Arn AppSyncApi: Type: AWS::AppSync::GraphQLApi Properties: Name: !Sub "CallAnalytics-${AWS::StackName}" AuthenticationType: AMAZON_COGNITO_USER_POOLS UserPoolConfig: AppIdClientRegex: !Ref UserPoolClient AwsRegion: !Ref AWS::Region UserPoolId: !Ref UserPool # XXX change to DENY and explicitly add access (e.g. Supervisors, Agent) DefaultAction: ALLOW AdditionalAuthenticationProviders: - AuthenticationType: AWS_IAM LogConfig: CloudWatchLogsRoleArn: !GetAtt AppSyncCwlRole.Arn ExcludeVerboseContent: FALSE FieldLogLevel: ALL # Commenting out Xray as it causes issues with the ServiceLinked role on first deployment # similar to: https://github.com/aws/aws-cdk/issues/16598 # XrayEnabled: true AppSyncSchema: Type: AWS::AppSync::GraphQLSchema Properties: ApiId: !GetAtt AppSyncApi.ApiId DefinitionS3Location: ../source/appsync/schema.graphql AppSyncDataSource: Type: AWS::AppSync::DataSource Properties: ApiId: !GetAtt AppSyncApi.ApiId Name: CallEventSourcing Description: Call Analytics Event Sourcing DynamoDB Table Type: AMAZON_DYNAMODB ServiceRoleArn: !GetAtt AppSyncDynamoDbRole.Arn DynamoDBConfig: TableName: !Ref EventSourcingTable AwsRegion: !Ref AWS::Region AppSyncApiCache: Type: AWS::AppSync::ApiCache Properties: ApiId: !GetAtt AppSyncApi.ApiId Type: R4_LARGE ApiCachingBehavior: PER_RESOLVER_CACHING Ttl: 30 TransitEncryptionEnabled: false AtRestEncryptionEnabled: false GetCallAppSyncResolver: Type: AWS::AppSync::Resolver DependsOn: AppSyncSchema Properties: ApiId: !GetAtt AppSyncApi.ApiId DataSourceName: !GetAtt AppSyncDataSource.Name TypeName: Query FieldName: getCall RequestMappingTemplateS3Location: ../source/appsync/getCall.request.vtl ResponseMappingTemplate: $util.toJson($context.result) CreateCallAppSyncResolver: Type: AWS::AppSync::Resolver DependsOn: AppSyncSchema Properties: ApiId: !GetAtt AppSyncApi.ApiId DataSourceName: !GetAtt AppSyncDataSource.Name TypeName: Mutation FieldName: createCall # This VTL uses a local sub so it needs to be kept here RequestMappingTemplate: !Sub |- #set( $shardsInDay = 6 ) #set( $shardDivider = 24 / $shardsInDay ) #set( $Integer = 0 ) #set( $now = $util.time.nowISO8601() ) #set( $date = $now.substring(0, 10) ) #set( $hourString = $now.substring(11, 13) ) #set( $hour = $Integer.parseInt($hourString) ) #set( $hourShard = $hour / $shardDivider ) #set( $shardPad = $date.format("%02d", $hourShard) ) #set( $PK = "c#${!ctx.args.input.CallId}" ) #set( $listPk = "cls#${!date}#s#${!shardPad}" ) #set( $listSk = "ts#${!now}#id#${!ctx.args.input.CallId}" ) #set( $CreatedAt = $util.defaultIfNullOrBlank($ctx.args.input.CreatedAt, $util.time.nowISO8601()) ) $util.qr($ctx.args.input.put("UpdatedAt", $now)) $util.qr($ctx.args.input.put("Status", "STARTED")) { "version" : "2018-05-29", "operation" : "TransactWriteItems", "transactItems": [ { "table": "${EventSourcingTable}", "operation": "PutItem", "key" : { "PK": $util.dynamodb.toDynamoDBJson($PK), "SK": $util.dynamodb.toDynamoDBJson($PK), }, "attributeValues": $util.dynamodb.toMapValuesJson($ctx.args.input), "condition": { "expression": "attribute_not_exists(#PK)", "expressionNames": { "#PK": "PK", }, }, }, { "table": "${EventSourcingTable}", "operation": "PutItem", "key" : { "PK": $util.dynamodb.toDynamoDBJson($listPk), "SK": $util.dynamodb.toDynamoDBJson($listSk), }, "attributeValues": { "CallId": $util.dynamodb.toDynamoDBJson($ctx.args.input.CallId), "CreatedAt": $util.dynamodb.toDynamoDBJson($ctx.args.input.CreatedAt), "UpdatedAt": $util.dynamodb.toDynamoDBJson($ctx.args.input.UpdatedAt), "ExpiresAfter": $util.dynamodb.toDynamoDBJson($ctx.args.input.ExpiresAfter) }, }, ], } ResponseMappingTemplateS3Location: ../source/appsync/createCall.response.vtl UpdateCallStatusAppSyncResolver: Type: AWS::AppSync::Resolver DependsOn: AppSyncSchema Properties: ApiId: !GetAtt AppSyncApi.ApiId DataSourceName: !GetAtt AppSyncDataSource.Name TypeName: Mutation FieldName: updateCallStatus RequestMappingTemplateS3Location: ../source/appsync/updateCall.request.vtl ResponseMappingTemplateS3Location: ../source/appsync/dDbPutCondition.response.vtl UpdateRecordingUrlAppSyncResolver: Type: AWS::AppSync::Resolver DependsOn: AppSyncSchema Properties: ApiId: !GetAtt AppSyncApi.ApiId DataSourceName: !GetAtt AppSyncDataSource.Name TypeName: Mutation FieldName: updateRecordingUrl RequestMappingTemplateS3Location: ../source/appsync/updateCall.request.vtl ResponseMappingTemplateS3Location: ../source/appsync/dDbPutCondition.response.vtl UpdatePcaUrlAppSyncResolver: Type: AWS::AppSync::Resolver DependsOn: AppSyncSchema Properties: ApiId: !GetAtt AppSyncApi.ApiId DataSourceName: !GetAtt AppSyncDataSource.Name TypeName: Mutation FieldName: updatePcaUrl RequestMappingTemplateS3Location: ../source/appsync/updateCall.request.vtl ResponseMappingTemplateS3Location: ../source/appsync/dDbPutCondition.response.vtl UpdateAgentAppSyncResolver: Type: AWS::AppSync::Resolver DependsOn: AppSyncSchema Properties: ApiId: !GetAtt AppSyncApi.ApiId DataSourceName: !GetAtt AppSyncDataSource.Name TypeName: Mutation FieldName: updateAgent RequestMappingTemplateS3Location: ../source/appsync/updateCall.request.vtl ResponseMappingTemplateS3Location: ../source/appsync/dDbPutCondition.response.vtl AddCallCategoryAppSyncResolver: Type: AWS::AppSync::Resolver DependsOn: AppSyncSchema Properties: ApiId: !GetAtt AppSyncApi.ApiId DataSourceName: !GetAtt AppSyncDataSource.Name TypeName: Mutation FieldName: addCallCategory RequestMappingTemplateS3Location: ../source/appsync/updateCall.request.vtl ResponseMappingTemplateS3Location: ../source/appsync/dDbPutCondition.response.vtl AddIssuesDetectedAppSyncResolver: Type: AWS::AppSync::Resolver DependsOn: AppSyncSchema Properties: ApiId: !GetAtt AppSyncApi.ApiId DataSourceName: !GetAtt AppSyncDataSource.Name TypeName: Mutation FieldName: addIssuesDetected RequestMappingTemplateS3Location: ../source/appsync/updateCall.request.vtl ResponseMappingTemplateS3Location: ../source/appsync/dDbPutCondition.response.vtl AddCallSummaryTextAppSyncResolver: Type: AWS::AppSync::Resolver DependsOn: AppSyncSchema Properties: ApiId: !GetAtt AppSyncApi.ApiId DataSourceName: !GetAtt AppSyncDataSource.Name TypeName: Mutation FieldName: addCallSummaryText RequestMappingTemplateS3Location: ../source/appsync/updateCall.request.vtl ResponseMappingTemplateS3Location: ../source/appsync/dDbPutCondition.response.vtl UpdateCallAggregationAppSyncResolver: Type: AWS::AppSync::Resolver DependsOn: AppSyncSchema Properties: ApiId: !GetAtt AppSyncApi.ApiId DataSourceName: !GetAtt AppSyncDataSource.Name TypeName: Mutation FieldName: updateCallAggregation RequestMappingTemplateS3Location: ../source/appsync/updateCall.request.vtl ResponseMappingTemplateS3Location: ../source/appsync/dDbPutCondition.response.vtl AddTranscriptSegmentAppSyncResolver: Type: AWS::AppSync::Resolver DependsOn: AppSyncSchema Properties: ApiId: !GetAtt AppSyncApi.ApiId DataSourceName: !GetAtt AppSyncDataSource.Name TypeName: Mutation FieldName: addTranscriptSegment RequestMappingTemplateS3Location: ../source/appsync/addTranscriptSegment.request.vtl ResponseMappingTemplateS3Location: ../source/appsync/dDbPutCondition.response.vtl ListCallsDateHourAppSyncResolver: Type: AWS::AppSync::Resolver DependsOn: AppSyncSchema Properties: ApiId: !GetAtt AppSyncApi.ApiId DataSourceName: !GetAtt AppSyncDataSource.Name TypeName: Query FieldName: listCallsDateHour RequestMappingTemplateS3Location: ../source/appsync/listCallsDateHour.request.vtl ResponseMappingTemplateS3Location: ../source/appsync/listCalls.response.vtl ListCallsDateShardAppSyncResolver: Type: AWS::AppSync::Resolver DependsOn: AppSyncSchema Properties: ApiId: !GetAtt AppSyncApi.ApiId DataSourceName: !GetAtt AppSyncDataSource.Name TypeName: Query FieldName: listCallsDateShard RequestMappingTemplateS3Location: ../source/appsync/listCallsDateShard.request.vtl ResponseMappingTemplateS3Location: ../source/appsync/listCalls.response.vtl ListCallsAppSyncResolver: Type: AWS::AppSync::Resolver DependsOn: AppSyncSchema Properties: ApiId: !GetAtt AppSyncApi.ApiId DataSourceName: !GetAtt AppSyncDataSource.Name TypeName: Query FieldName: listCalls RequestMappingTemplateS3Location: ../source/appsync/listCalls.request.vtl ResponseMappingTemplateS3Location: ../source/appsync/listCalls.response.vtl GetTranscriptSegmentsAppSyncResolver: Type: AWS::AppSync::Resolver DependsOn: AppSyncSchema Properties: ApiId: !GetAtt AppSyncApi.ApiId DataSourceName: !GetAtt AppSyncDataSource.Name TypeName: Query FieldName: getTranscriptSegments RequestMappingTemplateS3Location: ../source/appsync/getTranscriptSegments.request.vtl ResponseMappingTemplateS3Location: ../source/appsync/getTranscriptSegments.response.vtl GetTranscriptSegmentsWithSentimentAppSyncResolver: Type: AWS::AppSync::Resolver DependsOn: AppSyncSchema Properties: ApiId: !GetAtt AppSyncApi.ApiId DataSourceName: !GetAtt AppSyncDataSource.Name TypeName: Query FieldName: getTranscriptSegmentsWithSentiment RequestMappingTemplateS3Location: ../source/appsync/getTranscriptSegmentsWithSentiment.request.vtl ResponseMappingTemplateS3Location: ../source/appsync/getTranscriptSegmentsWithSentiment.response.vtl ########################################################################## # Cognito # Sample Cognito resources ########################################################################## UserPool: Type: AWS::Cognito::UserPool Properties: AdminCreateUserConfig: AllowAdminCreateUserOnly: !If - ShouldAllowSignUpEmailDomain - false - true InviteMessageTemplate: EmailSubject: Welcome to Live Call Analytics with Agent Assist! EmailMessage: !Sub |-

Hello {username},

Welcome to Live Call Analytics with Agent Assist! Your temporary password is:

{####}

When the CloudFormation stack is COMPLETE, use the link below to log in to the Live Call Analytics web interface and set your permanent password.

https://${WebAppCloudFrontDistribution.DomainName}/

Good luck!

Live Call Analytics with Agent Assist (amazon.com/live-call-analytics) AutoVerifiedAttributes: - email EmailConfiguration: EmailSendingAccount: COGNITO_DEFAULT EmailVerificationMessage: >- Please verify your email to complete account registration. Confirmation Code {####}. EmailVerificationSubject: >- Account Verification LambdaConfig: !If - ShouldAllowSignUpEmailDomain - PreAuthentication: !GetAtt CognitoUserPoolEmailDomainVerifyFunction.Arn PreSignUp: !GetAtt CognitoUserPoolEmailDomainVerifyFunction.Arn - !Ref AWS::NoValue Policies: PasswordPolicy: MinimumLength: 8 RequireLowercase: true RequireNumbers: true RequireSymbols: true RequireUppercase: true Schema: - Name: email AttributeDataType: String Mutable: false Required: true UserPoolName: !Sub "${AWS::StackName}-UserPool" UsernameAttributes: - email UserPoolClient: Type: AWS::Cognito::UserPoolClient Properties: AccessTokenValidity: 1 ClientName: !Sub "${AWS::StackName}-Client" EnableTokenRevocation: true ExplicitAuthFlows: - ALLOW_USER_SRP_AUTH - ALLOW_REFRESH_TOKEN_AUTH GenerateSecret: false IdTokenValidity: 1 PreventUserExistenceErrors: ENABLED ReadAttributes: - email - email_verified - preferred_username RefreshTokenValidity: 30 SupportedIdentityProviders: - COGNITO UserPoolId: !Ref UserPool IdentityPool: Type: AWS::Cognito::IdentityPool Properties: IdentityPoolName: !Sub "${AWS::StackName}-IdentityPool" AllowUnauthenticatedIdentities: false CognitoIdentityProviders: - ClientId: !Ref UserPoolClient ProviderName: !GetAtt UserPool.ProviderName CognitoIdentityPoolSetRole: Type: AWS::Cognito::IdentityPoolRoleAttachment Properties: IdentityPoolId: !Ref IdentityPool Roles: authenticated: !GetAtt CognitoAuthorizedRole.Arn CognitoAuthorizedRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Federated: cognito-identity.amazonaws.com Action: - sts:AssumeRoleWithWebIdentity Condition: StringEquals: "cognito-identity.amazonaws.com:aud": !Ref IdentityPool "ForAnyValue:StringLike": "cognito-identity.amazonaws.com:amr": authenticated Policies: - PolicyName: accessS3RecordingsPolicy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - "s3:GetObject" Resource: - !Sub - "arn:aws:s3:::${bucket}" - bucket: !If - ShouldCreateRecordingBucket - !Ref RecordingsBucket - !Ref S3BucketName - !Sub - "arn:aws:s3:::${bucket}/*" - bucket: !If - ShouldCreateRecordingBucket - !Ref RecordingsBucket - !Ref S3BucketName - PolicyName: accessParameterStore PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - ssm:GetParameter Resource: - !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${LCASettingsParameter}" - PolicyName: accessTranslateAPI PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - translate:TranslateText - comprehend:DetectDominantLanguage Resource: "*" Metadata: cfn_nag: rules_to_suppress: - id: W12 reason: comprehend, translate, and connect do not support resource-level permissions AdminUser: Type: AWS::Cognito::UserPoolUser DependsOn: CognitoUserPoolEmailDomainVerifyPermissionReady Properties: DesiredDeliveryMediums: - EMAIL UserAttributes: - Name: email Value: !Ref AdminEmail Username: !Ref AdminEmail UserPoolId: !Ref UserPool CognitoUserPoolEmailDomainVerifyPermissionReady: Type: AWS::CloudFormation::WaitConditionHandle Metadata: SecondaryCIDRready: !If [ ShouldAllowSignUpEmailDomain, !Ref CognitoUserPoolEmailDomainVerifyPermission, "" ] AdminGroup: Type: AWS::Cognito::UserPoolGroup Properties: Description: Administrators GroupName: Admin Precedence: 0 UserPoolId: !Ref UserPool AdminUserToGroupAttachment: Type: AWS::Cognito::UserPoolUserToGroupAttachment Properties: GroupName: !Ref AdminGroup Username: !Ref AdminUser UserPoolId: !Ref UserPool CognitoUserPoolEmailDomainVerifyFunction: Type: AWS::Serverless::Function Condition: ShouldAllowSignUpEmailDomain Properties: Handler: index.handler Runtime: nodejs14.x Timeout: 3 Environment: Variables: ALLOWED_SIGNUP_EMAIL_DOMAINS: !Ref AllowedSignUpEmailDomain InlineCode: | exports.handler = async (event, context) => { console.log(event); const allowed_domains = ( process.env?.ALLOWED_SIGNUP_EMAIL_DOMAINS .split(",").map(domain => {return domain.trim()}) ); const { email } = event.request?.userAttributes; if (!email || !email.includes('@')) { throw Error('Username does not exists or invalid email address'); } const emailDomain = email?.split('@')[1]; if (!emailDomain || !allowed_domains) { throw new Error('Server error - invalid configuration'); } if (!allowed_domains.includes(emailDomain)) { throw new Error('Invalid email address domain'); } return event; }; Metadata: cfn_nag: rules_to_suppress: - id: W89 reason: Customer can use VPC if desired CognitoUserPoolEmailDomainVerifyPermission: Type: AWS::Lambda::Permission Condition: ShouldAllowSignUpEmailDomain Properties: Action: lambda:InvokeFunction FunctionName: !Ref CognitoUserPoolEmailDomainVerifyFunction Principal: cognito-idp.amazonaws.com SourceAccount: !Ref AWS::AccountId SourceArn: !GetAtt UserPool.Arn ########################################################################## # Web Site ########################################################################## # Custom resource to empty and delete WebApp bucket when stack is deleted BucketDeleteLambdaRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Policies: - PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - "s3:DeleteBucket" - "s3:ListBucket" - "s3:ListBucketVersions" - "s3:DeleteObject" - "s3:DeleteObjectVersion" Resource: - !Sub "arn:aws:s3:::${WebAppBucket}" - !Sub "arn:aws:s3:::${WebAppBucket}/*" PolicyName: deleteBucketS3Policy BucketDeleteLambda: Metadata: cfn_nag: rules_to_suppress: - id: W89 reason: Customer can use VPC if desired Type: AWS::Lambda::Function Properties: Handler: "index.lambda_handler" Runtime: python3.9 MemorySize: 128 Timeout: 60 Role: !GetAtt BucketDeleteLambdaRole.Arn Code: ZipFile: | import boto3 import cfnresponse def deleteS3Bucket(bucketName): print("Deleting S3 Bucket %s" % bucketName) bucket = boto3.resource("s3").Bucket(bucketName) bucket.object_versions.all().delete() bucket.delete() def lambda_handler(event, context): print(event) responseData = {} status = cfnresponse.SUCCESS reason = "Success" if event['RequestType'] == 'Delete': try: deleteS3Bucket(event['ResourceProperties']['BucketName']) except Exception as e: print(e) reason = f"Exception thrown: {e}" status = cfnresponse.FAILED cfnresponse.send(event, context, status, responseData, reason=reason) RemoveWebAppBucketOnDelete: Type: Custom::RemoveWebAppBucketOnDelete Properties: ServiceToken: !GetAtt BucketDeleteLambda.Arn BucketName: !Ref WebAppBucket WebAppBucket: Type: AWS::S3::Bucket Properties: AccessControl: Private BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 # XXX figure out access logging # LoggingConfiguration: # DestinationBucketName: !Ref RecordingsBucket # LogFilePrefix: logs/ PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true VersioningConfiguration: Status: Enabled WebsiteConfiguration: IndexDocument: index.html ErrorDocument: index.html WebAppBucketPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: !Ref WebAppBucket PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId Action: s3:GetObject Resource: !Sub "${WebAppBucket.Arn}/*" - Effect: "Deny" Action: - "s3:*" Principal: "*" Resource: - !GetAtt WebAppBucket.Arn - !Sub "${WebAppBucket.Arn}/*" Condition: Bool: "aws:SecureTransport": false CloudFrontOriginAccessIdentity: Type: AWS::CloudFront::CloudFrontOriginAccessIdentity Properties: CloudFrontOriginAccessIdentityConfig: Comment: !Sub "CloudFront OAI for ${WebAppBucket}" WebAppCloudFrontDistribution: Type: AWS::CloudFront::Distribution Properties: DistributionConfig: Comment: !Sub "Web app cloudfront distribution ${AWS::StackName}" CustomErrorResponses: # Send errors to index file - ErrorCachingMinTTL: 300 ErrorCode: 403 ResponseCode: 200 ResponsePagePath: /index.html - ErrorCachingMinTTL: 300 ErrorCode: 404 ResponseCode: 200 ResponsePagePath: /index.html DefaultCacheBehavior: AllowedMethods: - GET - HEAD - OPTIONS Compress: true ForwardedValues: QueryString: false Cookies: Forward: none TargetOriginId: webapp-s3-bucket ViewerProtocolPolicy: redirect-to-https DefaultTTL: 600 MinTTL: 300 MaxTTL: 900 DefaultRootObject: index.html Enabled: true HttpVersion: http2 IPV6Enabled: true # Logging: # Bucket: !GetAtt RecordingsBucket.DomainName # IncludeCookies: false # Prefix: logs/cloudfront/ Origins: - Id: webapp-s3-bucket DomainName: !GetAtt WebAppBucket.RegionalDomainName S3OriginConfig: OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}" PriceClass: !Ref CloudFrontPriceClass Restrictions: !If - ShouldEnableGeoRestriction - GeoRestriction: RestrictionType: whitelist Locations: !Split [',', !Ref CloudFrontAllowedGeos] - GeoRestriction: RestrictionType: none Metadata: cfn_nag: rules_to_suppress: - id: W70 reason: This is using Cloudfront default TLS, can be changed by customer if needed.