AWSTemplateFormatVersion: 2010-09-09 

###########################################################################################################
############## DESCRIPTION ##############
###########################################################################################################

Description: This CloudFormation template deploys the infrastructure needed to deploy GenAI enabled self-service using Amazon Lex, Amazon Bedrock, etc.  

###########################################################################################################
############## METADATA ##############
###########################################################################################################

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Lambda Resources Details
        Parameters:
          - LambdaS3Bucket
          - LambdaLayerOpenSearchS3Object
          - LambdaLayerOpenSearchS3ObjectVersionId
          - LambdaServiceNowS3Object
          - LambdaServiceNowS3ObjectVersionId
      - Label:
          default: ServiceNow Details
        Parameters:
          - ServiceNowSecretName
          - ServiceNowHost
          - ServiceNowUsername
      - Label:
          default: OpenSearch Serverless Collection Details
        Parameters:
          - VectorIndexName
          - OssCollectionName
          - OssCollectionDataAccessPolicyName
          - OssCollectionNetAccessPolicyName
          - OssCollectionEncAccessPolicyName
      - Label:
          default: Bedrock Resources Details
        Parameters:
          - BedrockKnowledgeBaseName
          - BedrockDataSourceName
      - Label:
          default: Lex Bot Resources Details
        Parameters:
          - LexBotName
          - LexDraftVersion
          - LexBotCwLogRetentionDays
          - LexBotLogDeletionPolicy
          - LexBotLogUpdateReplacePolicy

###########################################################################################################
############## PARAMETERS ##############
###########################################################################################################

Parameters:
  VectorIndexName:
    Type: String
    Description: The name of the vector index to store the embeddings.
    Default: vector-index

  OssCollectionName:
    Type: String
    Description: The name of the OpenSearch Serverless collection to store the embeddings.
    Default: oss-collection
    MinLength: 3
    MaxLength: 32
    AllowedPattern: '^[a-z][a-z0-9-]{2,31}$'
    ConstraintDescription: Collection names must start with a lowercase letter, unique to your account and AWS Region, contains between 3 and 28 characters, contains only lowercase letters a-z, the numbers 0-9, and the hyphen

  OssCollectionDataAccessPolicyName:
    Type: String
    Description: The name of the OpenSearch Serverless access policy to store the embeddings.
    Default: oss-access-policy
    MinLength: 3
    MaxLength: 32
    AllowedPattern: '^[a-z][a-z0-9-]{2,31}$'
    ConstraintDescription: Access policy names must start with a lowercase letter, unique to your account and AWS Region, contains between 3 and 28 characters, contains only lowercase letters a-z, the numbers 0-9, and the hyphen

  OssCollectionNetAccessPolicyName:
    Type: String
    Description: The name of the OpenSearch Serverless network access policy to store the embeddings.
    Default: oss-net-access-policy
    MinLength: 3
    MaxLength: 32
    AllowedPattern: '^[a-z][a-z0-9-]{2,31}$'
    ConstraintDescription: Network policy names must start with a lowercase letter, unique to your account and AWS Region, contains between 3 and 28 characters, contains only lowercase letters a-z, the numbers 0-9, and the hyphen

  OssCollectionEncAccessPolicyName:
    Type: String
    Description: The name of the OpenSearch Serverless encryption access policy to store the embeddings.
    Default: oss-enc-access-policy
    MinLength: 3
    MaxLength: 32
    AllowedPattern: '^[a-z][a-z0-9-]{2,31}$'
    ConstraintDescription: Encryption policy names must start with a lowercase letter, unique to your account and AWS Region, contains between 3 and 28 characters, contains only lowercase letters a-z, the numbers 0-9, and the hyphen

  LambdaS3Bucket:
    Type: String
    Description: The S3 bucket where the Lambda resources are stored.
    Default: aws-blogs-artifacts-public
    MinLength: 3
    MaxLength: 63

  LambdaLayerOpenSearchS3Object:
    Type: String
    Description: The S3 object of .zip file where the OpenSearch Lambda layer is stored.
    Default: ML-16345/opensearch-layer.zip

  LambdaLayerOpenSearchS3ObjectVersionId:
    Type: String
    Description: The S3 object version ID of .zip file where the OpenSearch Lambda layer is stored.
    Default: oN10o8oG2pfiV3eFwX2WvpciOgc6LLGJ

  LambdaServiceNowS3Object:
    Type: String
    Description: The S3 object of .zip file where the ServiceNow Lambda function code is stored.
    Default: ML-16345/servicenowLambda.zip

  LambdaServiceNowS3ObjectVersionId:
    Type: String
    Description: The S3 object version ID of .zip file where the ServiceNow Lambda function code is stored.
    Default: PcJFp0tIeSHIeZFgt4Oaqe9E78L.ZO72

  ServiceNowSecretName:
    Type: String
    Description: The name of the ServiceNow secret in AWS Secrets Manager
    Default: servicenowCredentials

  ServiceNowHost: 
    Type: String
    Description: The ServiceNow host to connect to. Example value could be example.service-now.com
    AllowedPattern: ^(?!https:\/\/)[^.]+\.(service-now\.com)$
    ConstraintDescription: Must be a valid ServiceNow host in the format example.service-now.com

  ServiceNowUsername:
    Type: String
    Description: The ServiceNow username to use for authentication
    Default: admin

  BedrockKnowledgeBaseName:
    Type: String
    Description: The name of the Bedrock Knowledge Base to store the embeddings.
    Default: servicenow-bedrock-knowledge-base
    AllowedPattern: '^([0-9a-zA-Z][_-]?){1,100}$'
    ConstraintDescription: Knowledge base names must be between 1 and 100 characters, and can contain any combination of alphanumeric characters and the symbols _ and -
    
  BedrockDataSourceName:
    Type: String
    Description: The name of the Bedrock Data Source to store the embeddings.
    Default: servicenow-bedrock-data-source
    AllowedPattern: '^([0-9a-zA-Z][_-]?){1,100}$'
    ConstraintDescription: Data source names must be between 1 and 100 characters, and can contain any combination of alphanumeric characters and the symbols _ and -

  LexBotName:
    Type: String
    Description: The name of the Lex Bot to interact with the user.
    Default: servicenow-helpdesk-bot
    AllowedPattern: '^([0-9a-zA-Z][_-]?)+$'
    ConstraintDescription: Bot names must be between 1 and 50 characters, and can contain any combination of alphanumeric characters and the symbols _ and -
    MinLength: 1
    MaxLength: 100

  LexDraftVersion:
    Type: String
    Description: The Default Draft version ID for Lex Bot
    Default: TSTALIASID

  LexBotCwLogRetentionDays:
    Type: Number
    Description: The number of days to retain the CloudWatch Logs for the Lex Bot.
    Default: 30
    AllowedValues:
      - 1
      - 3
      - 5
      - 7
      - 14
      - 30
      - 60
      - 90
      - 120
      - 150
      - 180
      - 365
      - 400
      - 545
      - 731
      - 1096
      - 1827
      - 2192
      - 2557
      - 2922
      - 3288
      - 3653

  LexBotLogDeletionPolicy:
    Type: String
    Description: Deletion Policy for the Lex Bot CloudWatch Logs. 
    Default: Delete
    AllowedValues:
      - Delete
      - Retain

  LexBotLogUpdateReplacePolicy:
    Type: String
    Description: Update Replace Policy for the Lex Bot CloudWatch Logs. 
    Default: Delete
    AllowedValues:
      - Delete
      - Retain

###########################################################################################################
############## RESOURCES ##############
###########################################################################################################

Resources:
#OpenSearch Serverless (OSS) Collection for Knowledge Base
  OssCollection:
    Type: AWS::OpenSearchServerless::Collection
    DependsOn:
      - OssCollectionDataAccessPolicy
      - OssCollectionNetworkAccessPolicy
      - OssCollectionEncryptionPolicy
    Properties:
      Name: !Ref OssCollectionName
      Description: Collection for Bedrock Knowledge Base
      Type: VECTORSEARCH
      StandbyReplicas: DISABLED

#Data Access Policy for OpenSearch Serverless Collection
  OssCollectionDataAccessPolicy:
    Type: AWS::OpenSearchServerless::AccessPolicy
    Properties:
      Name: !Ref OssCollectionDataAccessPolicyName
      Description: Data Access Policy for OpenSearch Serverless Collection
      Policy: !Sub |
        [
          {
            "Rules": [
              {
                "Resource": [
                  "collection/${OssCollectionName}"
                ],
                "Permission": [
                  "aoss:DescribeCollectionItems",
                  "aoss:CreateCollectionItems",
                  "aoss:UpdateCollectionItems"
                ],
                "ResourceType": "collection"
              },
              {
                "Resource": [
                  "index/${OssCollectionName}/*"
                ],
                "Permission": [
                  "aoss:UpdateIndex",
                  "aoss:DescribeIndex",
                  "aoss:ReadDocument",
                  "aoss:WriteDocument",
                  "aoss:CreateIndex",
                  "aoss:DeleteIndex"
                ],
                "ResourceType": "index"
              }
            ],
            "Principal": [
              "${KbServiceRole.Arn}",
              "arn:${AWS::Partition}:iam::${AWS::AccountId}:root"
            ],
            "Description": "Data Access Policy for OpenSearch Serverless Collection"
          }
        ]
      Type: data

#Network Access Policy for OpenSearch Serverless Collection
  OssCollectionNetworkAccessPolicy:
    Type: AWS::OpenSearchServerless::SecurityPolicy
    Properties:
      Name: !Ref OssCollectionNetAccessPolicyName
      Description: Network Access Policy for OpenSearch Serverless Collection
      Policy: !Sub |
        [
          {
            "Rules": [
              {
                "Resource": [
                  "collection/${OssCollectionName}"
                ],
                "ResourceType": "collection"
              },
              {
                "Resource": [
                  "collection/${OssCollectionName}"
                ],
                "ResourceType": "dashboard"
              }
            ],
            "AllowFromPublic": true
          }
        ]
      Type: network

#Encryption Policy for OpenSearch Serverless Collection
  OssCollectionEncryptionPolicy:
    Type: AWS::OpenSearchServerless::SecurityPolicy
    Properties:
      Name: !Ref OssCollectionEncAccessPolicyName
      Description: Encryption Policy for OpenSearch Serverless Collection
      Policy: !Sub |
        {
          "Rules": [
            {
              "Resource": [
                "collection/${OssCollectionName}"
              ],
              "ResourceType": "collection"
            }
          ],
          "AWSOwnedKey": true
        }
      Type: encryption

#Custom Resource for creating Vector Index in OpenSearch Serverless Collection
  CreateVectorIndex:
    Type: Custom::CreateVectorIndex
    Properties:
      ServiceToken: !GetAtt CreateVectorIndexLambda.Arn

#Lambda function for creating Vector Index in OpenSearch Serverless Collection
  CreateVectorIndexLambda:
    Type: AWS::Lambda::Function
    Properties:
      Description: Lambda function for creating Vector Index in OpenSearch Serverless Collection
      Code:
        ZipFile: !Sub |
            import os 
            import json
            import boto3
            import sys
            import time
            import cfnresponse
            from opensearchpy import OpenSearch, RequestsHttpConnection, AWSV4SignerAuth, helpers
            from botocore.exceptions import ClientError

            if "LAMBDA_TASK_ROOT" in os.environ:
                envLambdaTaskRoot = os.environ["LAMBDA_TASK_ROOT"]
                sys.path.insert(0, "/var/lang/lib/python3.9/site-packages")

            def lambda_handler(event, context):
                print(event)
                host = os.environ['COLLECTION_ID']+".${AWS::Region}.aoss.${AWS::URLSuffix}"
                region = os.environ['AWS_REGION']
                credentials = boto3.Session().get_credentials()
                auth = AWSV4SignerAuth(credentials, region, 'aoss')
                print("OpenSearch client creation started...")
                client = OpenSearch(
                    hosts = [{"host": host, "port": 443}],
                    http_auth = auth,
                    use_ssl = True,
                    verify_certs = True,
                    connection_class = RequestsHttpConnection,
                    pool_maxsize = 20
                )       
                print("OpenSearch client created")
                index_name = os.environ['VECTOR_INDEX_NAME']
                index_body = {
                    "mappings": {
                        "properties": {
                            "AMAZON_BEDROCK_METADATA": {"type": "text"},
                            "AMAZON_BEDROCK_TEXT_CHUNK": {"type": "text"},
                            "embedding": {
                                "type": "knn_vector",
                                "dimension": 1536,
                                "method": {
                                    "engine": "faiss",
                                    "space_type": "l2",
                                    "name": "hnsw",
                                    "parameters": {"ef_construction": 5, "m": 2},
                                },
                            },
                        }
                    },
                    "settings": {
                        "index": {
                            "number_of_shards": 2,
                            "knn.algo_param": {"ef_search": 512},
                            "knn": True,
                        }
                    },
                }
                if event['RequestType'] == 'Create':
                    try:
                        print("Creating index in OpenSearch Serverless Collection...")
                        response = client.indices.create(index_name, body=index_body)
                        print(f'Vector index {index_name} has been created successfully in OpenSearch Collection')
                        print(response)
                    except Exception as ex:
                            print(ex) 
                    except ClientError as error:
                        time.sleep(10)
                        cfnresponse.send(event, context, cfnresponse.FAILED, {"code": error.response['Error']['Code']})
                        return
                if event['RequestType'] == 'Delete':
                    try:
                        print("Deleting index from OpenSearch Serverless Collection...")
                        response = client.indices.delete(index_name)
                        print(f'Vector index {index_name} has been deleted successfully from OpenSearch Collection')
                        print(response)
                    except Exception as ex:
                        print(ex) 
                    except ClientError as error:
                        time.sleep(10)
                        cfnresponse.send(event, context, cfnresponse.FAILED, {"code": error.response['Error']['Code']})
                        return

                time.sleep(10)
                cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
                return
      Handler: index.lambda_handler
      Runtime: python3.9
      Timeout: 300
      Role: !GetAtt CreateVectorIndexLambdaRole.Arn
      Layers:
        - !Ref CreateVectorIndexLambdaLayer
      Environment:
        Variables:
          COLLECTION_ID: !Ref OssCollection
          VECTOR_INDEX_NAME: !Ref VectorIndexName

#Lambda Layer for CreateVectorIndex Lambda function
  CreateVectorIndexLambdaLayer:
    Type: AWS::Lambda::LayerVersion
    Properties:
      Description: Lambda Layer for CreateVectorIndex Lambda function which contains opensearchpy Python package
      Content:
        S3Bucket: !Ref LambdaS3Bucket
        S3Key: !Ref LambdaLayerOpenSearchS3Object
        S3ObjectVersion: !Ref LambdaLayerOpenSearchS3ObjectVersionId
      CompatibleRuntimes:
        - python3.9

#IAM Role for Lambda function which creates vector index
  CreateVectorIndexLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      Description: IAM Role for Lambda function which creates vector index
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - !Sub 'lambda.${AWS::URLSuffix}'
            Action:
              - sts:AssumeRole
      Path: /service-role/
      ManagedPolicyArns:
        - !Sub 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
      Policies:
        - PolicyDocument: 
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - 'aoss:APIAccessAll'
                Resource: 
                  - !GetAtt OssCollection.Arn
          PolicyName: IndexCreationPermissions

#S3 Bucket for Knowledge DataSource
  KbDataSourceS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub 'servicenow-kb-ds-${AWS::AccountId}-${AWS::Region}'
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true

#Amazon Bedrock Knowledge base
  BedrockKnowledgeBase:
    DependsOn:
      - CreateVectorIndex
    Type: AWS::Bedrock::KnowledgeBase
    Properties:
      Name: !Ref BedrockKnowledgeBaseName
      Description: Amazon Bedrock Knowledge Base for ServiceNow
      KnowledgeBaseConfiguration:
        Type: VECTOR
        VectorKnowledgeBaseConfiguration:
          EmbeddingModelArn: !Sub 'arn:${AWS::Partition}:bedrock:${AWS::Region}::foundation-model/amazon.titan-embed-text-v1'
      RoleArn: !GetAtt KbServiceRole.Arn
      StorageConfiguration:
        Type: OPENSEARCH_SERVERLESS
        OpensearchServerlessConfiguration:
          CollectionArn: !GetAtt OssCollection.Arn
          VectorIndexName: !Ref VectorIndexName
          FieldMapping:
            MetadataField: AMAZON_BEDROCK_METADATA
            TextField: AMAZON_BEDROCK_TEXT_CHUNK
            VectorField: embedding

#Service Role for KnowledgeBase for Amazon Bedrock
  KbServiceRole:
    Type: AWS::IAM::Role
    Properties:
      Description: IAM Service Role for Amazon Bedrock Knowledge Base which can access Bedrock base models and the S3 objects containing data sources
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - !Sub 'bedrock.${AWS::URLSuffix}'
            Action:
              - sts:AssumeRole
            Condition:
              StringEquals:
                'aws:SourceAccount': !Ref AWS::AccountId
      Path: /service-role/

#IAM Policy for KnowledgeBase for Bedrock Service Role
  KbServiceRolePolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: KbServiceRolePolicy
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action:
              - s3:ListBucket
              - s3:GetObject
              - s3:PutObject
            Resource:
              - !Sub '${KbDataSourceS3Bucket.Arn}/*'
              - !Sub '${KbDataSourceS3Bucket.Arn}'
            Condition:
              StringEquals:
                'aws:PrincipalAccount': !Ref AWS::AccountId
          - Effect: Allow
            Action:
              - bedrock:ListFoundationModels
              - bedrock:ListCustomModels
            Resource: '*'
          - Effect: Allow
            Action:
              - bedrock:InvokeModel
            Resource:
              - !Sub 'arn:${AWS::Partition}:bedrock:${AWS::Region}::foundation-model/amazon.titan-embed-text*'
              - !Sub 'arn:${AWS::Partition}:bedrock:${AWS::Region}::foundation-model/anthropic.claude*'
          - Effect: Allow
            Action: 
              - bedrock:Retrieve
            Resource:
              - !Sub 'arn:${AWS::Partition}:bedrock:${AWS::Region}::knowledge-base/*'
          - Effect: Allow
            Action:
              - aoss:APIAccessAll
            Resource:
              - !GetAtt OssCollection.Arn
      Roles:
        - !Ref KbServiceRole

#Amazon Bedrock Knowledge Base Data Source
  KbDataSource:
    Type: AWS::Bedrock::DataSource
    Properties:
      Name: !Ref BedrockDataSourceName
      Description: Amazon Bedrock Data Source for ServiceNow articles
      DataDeletionPolicy: DELETE
      DataSourceConfiguration:
        S3Configuration:
          BucketArn: !GetAtt KbDataSourceS3Bucket.Arn
        Type: S3
      KnowledgeBaseId: !Ref BedrockKnowledgeBase
      VectorIngestionConfiguration:
        ChunkingConfiguration:
          ChunkingStrategy: FIXED_SIZE
          FixedSizeChunkingConfiguration:
            MaxTokens: 200
            OverlapPercentage: 50

#CloudWatch Log Group for Lex Chatbot Logs
  LexLogsCwLogGroup:
    Type: AWS::Logs::LogGroup
    DeletionPolicy: !Ref LexBotLogDeletionPolicy
    UpdateReplacePolicy: !Ref LexBotLogUpdateReplacePolicy
    Properties:
      RetentionInDays: !Ref LexBotCwLogRetentionDays

#Lex Bot to handle customer ServiceNow enquiries
  ServiceNowBot:
    Type: AWS::Lex::Bot
    Properties:
      Name: !Ref LexBotName
      Description: Lex Bot to handle customer ServiceNow enquiries
      RoleArn: !GetAtt LexBotRole.Arn
      TestBotAliasSettings:
        BotAliasLocaleSettings:
            - LocaleId: en_US
              BotAliasLocaleSetting:
                Enabled: true
                CodeHookSpecification:
                  LambdaCodeHook:
                    CodeHookInterfaceVersion: '1.0'
                    LambdaArn: !GetAtt ServiceNowLambda.Arn
        ConversationLogSettings:
          TextLogSettings:
            - Destination: 
                CloudWatch: 
                    CloudWatchLogGroupArn: !GetAtt LexLogsCwLogGroup.Arn
                    LogPrefix: aws/lex
              Enabled: true
      BotLocales:
        - LocaleId: en_US
          Description: Lex Bot to handle customer ServiceNow enquiries
          NluConfidenceThreshold: 0.4
          VoiceSettings:
            VoiceId: Danielle
            Engine: neural
          Intents:
            - Name: LogIncident
              Description: Logs Incident in the ServiceNow
              SampleUtterances:
                - Utterance: Open a ticket for {shortdescription}
                - Utterance: Create a ticket for {shortdescription}
                - Utterance: I am having trouble, please open a ticket for {shortdescription}
              InitialResponseSetting:
                CodeHook:
                  IsActive: true
                  EnableCodeHookInvocation: true
                  PostCodeHookSpecification:
                    SuccessNextStep:
                      DialogAction:
                        Type: ElicitSlot
                        SlotToElicit: shortdescription
                    FailureNextStep:
                      DialogAction:
                        Type: EndConversation
                    TimeoutNextStep:
                      DialogAction:
                        Type: EndConversation
                NextStep:
                  DialogAction:
                    Type: InvokeDialogCodeHook
              DialogCodeHook:
                Enabled: true
              FulfillmentCodeHook:
                Enabled: true
                IsActive: true
                PostFulfillmentStatusSpecification:
                  SuccessNextStep:
                    DialogAction:
                      Type: EndConversation
                  FailureNextStep:
                    DialogAction:
                      Type: EndConversation
                  TimeoutNextStep:
                    DialogAction:
                      Type: EndConversation
              SlotPriorities:
                - Priority: 1
                  SlotName: shortdescription
              Slots:
                - Name: shortdescription
                  SlotTypeName: AMAZON.AlphaNumeric
                  ObfuscationSetting:
                    ObfuscationSettingType: DefaultObfuscation
                  ValueElicitationSetting:
                    SlotConstraint: Required
                    SlotCaptureSetting:
                      CaptureNextStep:
                        DialogAction:
                          Type: FulfillIntent
                      FailureNextStep:
                        Intent:
                          Name: FallbackIntent
                        DialogAction:
                          Type: StartIntent
                      ElicitationCodeHook:
                        EnableCodeHookInvocation: true
                    PromptSpecification:
                      MessageSelectionStrategy: Random
                      AllowInterrupt: true
                      MessageGroupsList:
                        - Message:
                            PlainTextMessage:
                              Value: Short description for the ticket
                      MaxRetries: 4
            - Name: FallbackIntent
              Description: Default intent when no other intent matches.
              ParentIntentSignature: AMAZON.FallbackIntent
              InitialResponseSetting:
                CodeHook:
                  IsActive: true
                  EnableCodeHookInvocation: true
                  PostCodeHookSpecification:
                    SuccessNextStep:
                      DialogAction:
                        Type: EndConversation
                    FailureNextStep:
                      DialogAction:
                        Type: EndConversation
                    TimeoutNextStep:
                      DialogAction:
                        Type: EndConversation
                NextStep:
                  DialogAction:
                    Type: InvokeDialogCodeHook
      DataPrivacy:
        ChildDirected: false
      AutoBuildBotLocales: true
      IdleSessionTTLInSeconds: 300

#IAM Role for Lex Bot
  LexBotRole:
    Type: AWS::IAM::Role
    Properties:
      Description: IAM Role for Lex Bot
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - !Sub 'lexv2.${AWS::URLSuffix}'
            Action:
              - sts:AssumeRole
      Path: /service-role/

#IAM Policy for Lex Bot IAM Role
  LexBotRolePolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: LexBotRolePolicy
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action:
              - logs:CreateLogStream
              - logs:PutLogEvents
            Resource: !Sub 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:${LexLogsCwLogGroup}:log-stream:*'
          - Effect: Allow
            Action:
              - polly:SynthesizeSpeech
            Resource: '*'
          - Effect: Allow
            Action:
              - bedrock:InvokeModel
              - bedrock:InvokeModelWithResponseStream
            Resource:
              - !Sub 'arn:${AWS::Partition}:bedrock:${AWS::Region}::foundation-model/anthropic.claude*'
          - Effect: Allow
            Action:
              - bedrock:Retrieve
            Resource:
              - !GetAtt BedrockKnowledgeBase.KnowledgeBaseArn
      Roles:
        - !Ref LexBotRole

#Lex Bot Version for ServiceNow Bot
  ServiceNowBotVersion:
    Type: AWS::Lex::BotVersion
    Properties:
      BotId: !Ref ServiceNowBot
      Description: ServiceNow Lex Bot Version
      BotVersionLocaleSpecification:
        - LocaleId: en_US
          BotVersionLocaleDetails:
            SourceBotVersion: DRAFT

#Lex Bot Alias for ServiceNow Bot
  ServiceNowBotAlias:
    Type: AWS::Lex::BotAlias
    Properties:
      BotId: !Ref ServiceNowBot
      BotAliasName: !Sub '${LexBotName}-alias'
      Description: ServiceNow Lex bot Alias
      BotVersion: !GetAtt ServiceNowBotVersion.BotVersion
      BotAliasLocaleSettings:
        - LocaleId: en_US
          BotAliasLocaleSetting:
            Enabled: true
            CodeHookSpecification:
              LambdaCodeHook:
                CodeHookInterfaceVersion: '1.0'
                LambdaArn: !GetAtt ServiceNowLambda.Arn
      ConversationLogSettings:
        TextLogSettings:
          - Enabled: true
            Destination:
              CloudWatch:
                CloudWatchLogGroupArn: !GetAtt LexLogsCwLogGroup.Arn
                LogPrefix: aws/lex

#AWS Secrets Manager to store ServiceNow credentials
  ServiceNowCredsSecret:
    Type: AWS::SecretsManager::Secret
    Properties:
      Name: !Ref ServiceNowSecretName
      Description: This secret contains ServiceNow username and password.
      SecretString: !Sub '{"username": "${ServiceNowUsername}","password": ""}'

#Lambda function to interact with ServiceNow account
  ServiceNowLambda:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub 'servicenow-${AWS::AccountId}-${AWS::Region}'
      Description: Lambda function for invoking ServiceNow API to create a ticket
      Handler: index.handler
      Role: !GetAtt ServiceNowLambdaRole.Arn
      Timeout: 600
      MemorySize: 312
      ReservedConcurrentExecutions: 100
      Runtime: nodejs20.x
      Environment:
        Variables:
          SERVICENOW_HOST: !Ref ServiceNowHost
          SERVICENOW_USERNAME: !Ref ServiceNowUsername
          SECRET_NAME: !Ref ServiceNowSecretName
      Code:
        S3Bucket: !Ref LambdaS3Bucket
        S3Key: !Ref LambdaServiceNowS3Object
        S3ObjectVersion: !Ref LambdaServiceNowS3ObjectVersionId

#IAM Role for ServiceNow Lambda
  ServiceNowLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      Description: IAM Role for ServiceNow Lambda
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - !Sub 'lambda.${AWS::URLSuffix}'
            Action:
              - sts:AssumeRole
      Path: /service-role/
      ManagedPolicyArns:
        - !Sub 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'

#IAM Policy for ServiceNow Lambda Role
  ServiceNowLambdaRolePolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: ServiceNowLambdaRolePolicy
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action: 
              - secretsmanager:GetSecretValue
              - secretsmanager:GetResourcePolicy
              - secretsmanager:DescribeSecret
              - secretsmanager:ListSecrets
              - secretsmanager:ListSecretVersionIds
            Resource: 
              - !Ref ServiceNowCredsSecret
      Roles:
        - !Ref ServiceNowLambdaRole

#Permission for Lex Chatbot Custom Alias to invoke ServiceNow Lambda function
  BotServiceNowLambdaPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref ServiceNowLambda
      Principal: !Sub 'lexv2.${AWS::URLSuffix}'
      SourceAccount: !Ref AWS::AccountId
      SourceArn: !Sub 'arn:${AWS::Partition}:lex:${AWS::Region}:${AWS::AccountId}:bot-alias/${ServiceNowBot}/${ServiceNowBotAlias}'

#Permission for Lex Chatbot Test Alias to invoke ServiceNow Lambda function
  TestBotServiceNowLambdaPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref ServiceNowLambda
      Principal: !Sub 'lexv2.${AWS::URLSuffix}'
      SourceAccount: !Ref AWS::AccountId
      SourceArn: !Sub 'arn:${AWS::Partition}:lex:${AWS::Region}:${AWS::AccountId}:bot-alias/${ServiceNowBot}/${LexDraftVersion}'

###########################################################################################################
############## OUTPUTS ##############
###########################################################################################################

Outputs:
  BedrockKnowledgeBaseId:
    Description: Bedrock Knowledge Base ID
    Value: !Ref BedrockKnowledgeBase

  ServiceNowLambdaArn:
    Description: ServiceNow Lambda ARN
    Value: !GetAtt ServiceNowLambda.Arn

  ServiceNowBotArn:
    Description: ServiceNow Bot ARN
    Value: !GetAtt ServiceNowBotAlias.Arn

  LexBotId:
    Description: Lex Bot ID
    Value: !Ref ServiceNowBot

  LexBotAliasId:
    Description: Lex Bot Alias ID
    Value: !Ref ServiceNowBotAlias

  KbDataSourceS3BucketName:
    Description: S3 bucket name for uploading ServiceNow Articles
    Value: !Ref KbDataSourceS3Bucket

  OssCollectionEndpoint:
    Description: OpenSearch Serverless Collection-specific endpoint used to submit index, search, and data upload requests to an OpenSearch Serverless collection.
    Value: !GetAtt OssCollection.CollectionEndpoint

  OssDashboardEndpoint:
    Description: OpenSearch Serverless Collection-specific endpoint used to access OpenSearch Dashboards.
    Value: !GetAtt OssCollection.DashboardEndpoint

  ServiceNowCredsSecretUrl:
    Description: ServiceNow credentials secret URL
    Value: !Ref ServiceNowCredsSecret

  LexBotIntentConsoleUrl:
    Description: Lex Bot Intent URL
    Value: !Sub 'https://${AWS::Region}.console.aws.amazon.com/lexv2/home?region=${AWS::Region}#bot/${ServiceNowBot}/locale/en_US/intents'

  KnowledgeBaseConsoleUrl:
    Description: Knowledge Base Console URL
    Value: !Sub 'https://${AWS::Region}.console.aws.amazon.com/bedrock/home?region=${AWS::Region}#/knowledge-bases/${BedrockKnowledgeBaseName}/${BedrockKnowledgeBase}/0'

  LexCwLogGroupConsoleUrl:
    Description: Lex Bot CloudWatch Log Group Console URL
    Value: !Sub 'https://${AWS::Region}.console.aws.amazon.com/cloudwatch/home?region=${AWS::Region}#logsV2:log-groups/log-group/${LexLogsCwLogGroup}'