# Copyright (c) 2025 Amazon.com
# This file is licensed under the MIT License.
# See the LICENSE file in the project root for full license information.

---
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31

Description: Amazon Transcribe Live Meeting Assist - LMA (v0.2.21) (SO9497)

Parameters:
  Domain:
    Default: "Default"
    Type: String
    AllowedValues:
      - "Default"
      - "Healthcare"
    Description: Select the appropriate domain as necessary, or leave it at default

  MeetingAssistService:
    Default: "BEDROCK_LLM"
    Type: String
    AllowedValues:
      - "BEDROCK_LLM"
      - "STRANDS_BEDROCK"
      - "STRANDS_BEDROCK_WITH_KB (Create)"
      - "BEDROCK_KNOWLEDGE_BASE (Use Existing)"
      - "BEDROCK_AGENT (Use Existing)"
      - "Q_BUSINESS (Use Existing)"
      - "BEDROCK_KNOWLEDGE_BASE (Create)"
      - "BEDROCK_AGENT_WITH_KNOWLEDGE_BASE (Create)"
    Description: Choose the knowledge base / LLM service to use. STRANDS_BEDROCK (experimental) provides a lightweight alternative to QnABot with agentic capabilities. STRANDS_BEDROCK_WITH_KB automatically creates a Knowledge Base for the document search tool.

  MeetingAssistServiceBedrockModelID:
    Type: String
    Default: "anthropic.claude-3-haiku-20240307-v1:0"
    AllowedValues:
      - "anthropic.claude-3-haiku-20240307-v1:0"
      - "anthropic.claude-3-sonnet-20240229-v1:0"
      - "anthropic.claude-3-5-sonnet-20240620-v1:0"
      - "us.anthropic.claude-3-5-sonnet-20241022-v2:0"
      - "amazon.nova-micro-v1:0"
      - "amazon.nova-lite-v1:0"
      - "amazon.nova-pro-v1:0"
      - "us.amazon.nova-premier-v1:0"
      - "us.anthropic.claude-3-5-haiku-20241022-v1:0"
      - "us.anthropic.claude-3-7-sonnet-20250219-v1:0"
      - "us.anthropic.claude-sonnet-4-20250514-v1:0"
      - "us.anthropic.claude-opus-4-20250514-v1:0"
      - "us.anthropic.claude-opus-4-1-20250805-v1:0"
      - "us.anthropic.claude-sonnet-4-5-20250929-v1:0"
      - "eu.anthropic.claude-3-5-sonnet-20240620-v1:0"
      - "apac.anthropic.claude-3-5-sonnet-20240620-v1:0"
      - "global.anthropic.claude-sonnet-4-20250514-v1:0"
      - "global.anthropic.claude-haiku-4-5-20251001-v1:0"
      - "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
    Description: >-
      Select a Bedrock LLM model from the list. Includes Nova models (cost-effective), 
      Claude 3.5/3.7 (advanced), and Claude 4 (cutting-edge). Some models require inference profiles.

  TavilyApiKey:
    Type: String
    NoEcho: true
    Default: ""
    Description: >-
      (Optional - STRANDS_BEDROCK only) API key for Tavily web search service to enable web search tool.
      Get your API key from https://tavily.com. When provided, the Strands agent can search the web 
      for current information during meetings. Leave blank to disable web search capability.
    AllowedPattern: "^(|[a-zA-Z0-9_-]{20,})$"
    ConstraintDescription: "Must be empty or a valid Tavily API key (20+ characters)"

  BedrockGuardrailId:
    Type: String
    Default: ""
    AllowedPattern: "^(|[a-z0-9]+)$"
    Description: >
      If MeetingAssistService is 'BEDROCK_LLM' or 'BEDROCK_KNOWLEDGE_BASE', optionally provide the *Id* (not name) of an *existing* 
      Bedrock Guardrail to be used for Meeting Assist bot.

  BedrockGuardrailVersion:
    Type: String
    Default: "DRAFT"
    AllowedPattern: "^(|(([1-9][0-9]{0,7})|(DRAFT)))$"
    Description: >
      If MeetingAssistService is 'BEDROCK_LLM' or 'BEDROCK_KNOWLEDGE_BASE', and you provided a Bedrock Guardrail Id above, provide the 
      corresponding Guardrail version here.

  BedrockKnowledgeBaseId:
    Type: String
    Default: ""
    AllowedPattern: "^(|[0-9a-zA-Z]{10})$"
    Description: >
      If MeetingAssistService is 'BEDROCK_KNOWLEDGE_BASE (Use Existing)', provide the knowledge base
      *Id* (not name) of an *existing* Bedrock knowledge base to be used for Meeting
      Assist bot. If MeetingAssistService is 'BEDROCK_AGENT_WITH_KNOWLEDGE_BASE (Create)', optionally provide 
      the knowledge base *Id* of an existing Bedrock knowledge base for the new Bedrock Agent, or leave blank to
      create a new knowledge base.

  BedrockAgentId:
    Type: String
    Default: ""
    AllowedPattern: "^(|[0-9a-zA-Z]{10})$"
    Description: >
      If MeetingAssistService is 'BEDROCK_AGENT (Use Existing)', provide the agent
      *ID* (not name) of an *existing* Bedrock agent to be used for Meeting
      Assist bot.

  BedrockAgentAliasId:
    Type: String
    Default: "TSTALIASID"
    AllowedPattern: "^(|[0-9a-zA-Z]{10})$"
    Description: >
      If MeetingAssistService is 'BEDROCK_AGENT (Use Existing)', provide the agent
      Alias *ID* (not the Alias name) of an *existing* Bedrock agent to be used for Meeting
      Assist bot.

  BedrockKnowledgeBaseS3BucketName:
    Type: String
    Default: ""
    Description: >-
      If MeetingAssistService is 'BEDROCK_KNOWLEDGE_BASE (Create)', 'STRANDS_BEDROCK_WITH_KB (Create)', or 'BEDROCK_AGENT_WITH_KNOWLEDGE_BASE (Create)', provide the bucket 
      name of an existing bucket with documents to ingest, or leave blank if you don't need an S3 data source. If you provide an S3 bucket name an S3 data
      source will be created for you automatically.

  BedrockKnowledgeBaseS3DocumentUploadFolderPrefix:
    Type: String
    Default: ""
    Description: >-
      If MeetingAssistService is 'BEDROCK_KNOWLEDGE_BASE (Create)', 'STRANDS_BEDROCK_WITH_KB (Create)', or 'BEDROCK_AGENT_WITH_KNOWLEDGE_BASE (Create)', provide comma separated list of optional S3 prefixes 
      with the paths to your source documents e.g. "prefix1/, prefix2/". If you leave blank, all documents in your S3 bucket will be scanned.

  BedrockKnowledgeBaseWebCrawlerURLs:
    Type: String
    Default: https://en.wikipedia.org/wiki/Life_insurance, https://en.wikipedia.org/wiki/Mortgage_loan
    Description: >-
      If MeetingAssistService is 'BEDROCK_KNOWLEDGE_BASE (Create)', 'STRANDS_BEDROCK_WITH_KB (Create)', or 'BEDROCK_AGENT_WITH_KNOWLEDGE_BASE (Create)', provide comma separated list of public web sites to crawl,
      or leave blank if you don't need a Web data source. If you provide one or more Web URLs a Web crawler data source will be created for you automatically.

  BedrockKnowledgeBaseWebCrawlerScope:
    Type: String
    AllowedValues:
      - "DEFAULT"
      - "HOST_ONLY"
      - "SUBDOMAINS"
    Default: DEFAULT
    Description: >-
      If MeetingAssistService is 'BEDROCK_KNOWLEDGE_BASE (Create)', 'STRANDS_BEDROCK_WITH_KB (Create)', or 'BEDROCK_AGENT_WITH_KNOWLEDGE_BASE (Create)', specify the scope of what is crawled for your URLs.
      Choose DEFAULT to limit to web pages that have the same host and the same initial path as the source URLs.
      Choose HOST_ONLY to limit to web pages that have the same host as the source URLs.
      Choose SUBDOMAINS to to include sub domains in addition to the host or primary domain.

  AmazonQAppId:
    Type: String
    Default: ""
    AllowedPattern: "^(|[a-zA-Z0-9][a-zA-Z0-9-]{35})$"
    Description:
      If MeetingAssistService is 'Q_BUSINESS (Use Existing)', provide the Q Business Application
      *Id* (not name) of an *existing* Q Business Applictaion to be used for Meeting
      Assist bot.

  IDCApplicationARN:
    Type: String
    Description: >-
      After creating an Identity Center customer managed application for QBusiness, UPDATE your stack with its ARN entered here. 
      See Q Business setup README for directions.

  AssistantWakePhraseRegEx:
    Type: String
    Default: "(OK|Okay)[.,! ]*[Aa]ssistant"
    Description: >
      A regular expression for the wake phrase that invokes the Meeting Assistant.
      Default is 'OK Assistant'

  MeetingAssistQnABotOpenSearchNodeCount:
    Type: String
    Default: 1
    AllowedValues:
      - 1
      - 2
      - 4
    Description: >
      Number of nodes in Meeting Assistant QnABot OpenSearch domain - '4' is recommended
      for fault tolerant production deployments.

  TranscriptKnowledgeBase:
    Default: "BEDROCK_KNOWLEDGE_BASE (Create)"
    Type: String
    AllowedValues:
      - "DISABLED"
      - "BEDROCK_KNOWLEDGE_BASE (Create)"
    Description: Use meeting transcripts as knowledge base content

  S3BucketName:
    Type: String
    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

  AudioFilePrefix:
    Type: String
    Default: lma-audio-recordings/
    Description: >-
      The Amazon S3 prefix where the audio files will be saved (must end
      in "/")

  TranscriptFilePrefix:
    Type: String
    Default: lma-transcripts/
    Description: >-
      The Amazon S3 prefix where the transript files will be saved (must end
      in "/")

  IsPartialTranscriptEnabled:
    Type: String
    Default: "true"
    Description: >-
      Enable partial transcripts to receive low latency evolving transcriptions for
      each conversation turn. Set to false to process only the
      final version of each conversation turn.
    AllowedValues:
      - "true"
      - "false"

  IsContentRedactionEnabled:
    Type: String
    Default: "false"
    Description: >-
      Enable content redaction from Amazon Transcribe transcription output. This is
      only used when
      the 'en-US' language is selected in the TranscribeLanguageCode parameter.
    AllowedValues:
      - "true"
      - "false"

  TranscribeContentRedactionType:
    Type: String
    Default: PII
    Description: >-
      Type of content redaction from Amazon Transcribe transcription output
    AllowedValues:
      - PII

  TranscribeLanguageCode:
    Type: String
    Description: >-
      Language code or language identification setting for Amazon Transcribe. NOTE:
      Language identification can't  be combined with custom language models or redaction.

    Default: en-US
    AllowedValues:
      - identify-language
      - identify-multiple-languages
      - en-US
      - es-US
      - en-GB
      - fr-CA
      - fr-FR
      - en-AU
      - it-IT
      - de-DE
      - pt-BR
      - ja-JP
      - ko-KR
      - zh-CN
      - hi-IN
      - th-TH

  TranscribeLanguageOptions:
    Type: String
    Default: "en-US, es-US"
    AllowedPattern: '^(?:\s*(?:en-US|es-US|en-GB|fr-CA|fr-FR|en-AU|it-IT|de-DE|pt-BR|ja-JP|ko-KR|zh-CN|hi-IN|th-TH)\s*(?:,\s*(?:en-US|es-US|en-GB|fr-CA|fr-FR|en-AU|it-IT|de-DE|pt-BR|ja-JP|ko-KR|zh-CN|hi-IN|th-TH)\s*)*)?$'
    ConstraintDescription: >-
      Unsupported language code or invalid syntax. Allowed values are: en-US, es-US,
      en-GB, fr-CA, fr-FR, en-AU, it-IT, de-DE, pt-BR, ja-JP, ko-KR, zh-CN, hi-IN,
      th-TH
    Description: >-
      When using language identification, specify two or more language codes, e.g.
      "en-US, es-US" that represent the languages that may be  used in your meeting.
      Including more than five is not recommended.  Enter only one language dialect
      per language - e.g. 'en-US, en-AU' is not allowed.
      Allowed values are: en-US, es-US, en-GB, fr-CA, fr-FR, en-AU, it-IT, de-DE,
      pt-BR, ja-JP, ko-KR, zh-CN, hi-IN, th-TH
      NOTE: The identify-language feature has not yet been applied to the virtual participant. 
      If selected, the virtual participant will use the value of TranscribePreferredLanguage (if set) or 'en-US' (by default).

  TranscribePreferredLanguage:
    Type: String
    Description: >-
      Optionally select a preferred language when using language identification -
      must be one of the languages entered for TranscribeLanguageOptions.
    Default: None
    AllowedValues:
      - None
      - en-US
      - es-US
      - en-GB
      - fr-CA
      - fr-FR
      - en-AU
      - it-IT
      - de-DE
      - pt-BR
      - ja-JP
      - ko-KR
      - zh-CN
      - hi-IN
      - th-TH

  TranscribePiiEntityTypes:
    Type: String
    Default: BANK_ACCOUNT_NUMBER,BANK_ROUTING,CREDIT_DEBIT_NUMBER,CREDIT_DEBIT_CVV,CREDIT_DEBIT_EXPIRY,PIN,EMAIL,ADDRESS,NAME,PHONE,SSN
    Description: >-
      Select the PII entity types you want to identify or redact. Remove the values
      that you don't want to redact from the default.  DO NOT ADD CUSTOM VALUES HERE.

  CustomVocabularyName:
    Type: String
    Default: ""
    Description: >-
      The name of the vocabulary to use when processing the transcription job. Leave
      blank if no custom vocabulary to be used. If yes, the custom
      vocabulary must pre-exist in your account.

  CustomLanguageModelName:
    Type: String
    Default: ""
    Description: >-
      The name of the custom language model to use when processing the transcription
      job. Leave blank if no
      custom language model is to be used. If specified, the custom language model
      must pre-exist in your account,  match the Language Code selected above, and
      use the 'Narrow Band' base model.

  ModelValidation:
    Type: String
    Default: "true"
    AllowedValues:
      - "true"
      - "false"
    Description: >-
      Specifies if Amazon Bedrock model access is validated and sample package uploaded to QnABot.
      Setting this parameter to "false" is useful for automated deployments (e.g. AWS Workshop
      Studio) where Amazon Bedrock model access permissions may not be set up yet.

  TranscriptLambdaHookFunctionArn:
    Default: ""
    Type: String
    AllowedPattern: "^(|arn:aws:lambda:.*)$"
    Description: >
      (Optional) If present, the specified Lambda function is invoked by the LMA 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: "BEDROCK"
    Type: String
    AllowedValues:
      - "BEDROCK"
      - "LAMBDA"

    Description: >
      Set to enable call summarization by a Large Language Model. Currently only BEDROCK
      and LAMBDA are supported.
      BEDROCK option requires you to choose one of the supported model IDs from the  provided
      list (BedrockModelId). The LAMBDA option requires you to provide a function
      ARN below.

  BedrockModelId:
    Default: "anthropic.claude-3-haiku-20240307-v1:0"
    Type: String
    AllowedValues:
      - "anthropic.claude-3-haiku-20240307-v1:0"
      - "anthropic.claude-3-sonnet-20240229-v1:0"
      - "anthropic.claude-3-5-sonnet-20240620-v1:0"
      - "us.anthropic.claude-3-5-sonnet-20241022-v2:0"
      - "amazon.nova-micro-v1:0"
      - "amazon.nova-lite-v1:0"
      - "amazon.nova-pro-v1:0"
      - "us.amazon.nova-premier-v1:0"
      - "us.anthropic.claude-3-5-haiku-20241022-v1:0"
      - "us.anthropic.claude-3-7-sonnet-20250219-v1:0"
      - "us.anthropic.claude-sonnet-4-20250514-v1:0"
      - "us.anthropic.claude-opus-4-20250514-v1:0"
      - "us.anthropic.claude-opus-4-1-20250805-v1:0"
      - "us.anthropic.claude-sonnet-4-5-20250929-v1:0"
      - "eu.anthropic.claude-3-5-sonnet-20240620-v1:0"
      - "apac.anthropic.claude-3-5-sonnet-20240620-v1:0"
      - "global.anthropic.claude-sonnet-4-20250514-v1:0"
      - "global.anthropic.claude-haiku-4-5-20251001-v1:0"
      - "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
    Description: >
      If EndOfCallTranscriptSummary is BEDROCK, then choose a model ID from the list
      of supported models. Includes Nova models (cost-effective), Claude 3.5/3.7 (advanced), 
      and Claude 4 (cutting-edge). Some models require inference profiles. 
      Defaults to 'anthropic.claude-3-haiku-20240307-v1:0'

  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 LMA 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 LMA 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 LMA 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.

  AdminEmail:
    Type: String
    Description: >-
      Email address of the admin user (e.g. jdoe+admin@example.com) that has access to all meetings 
      for administrative purposes. 
      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})*)$'

  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

  EnableAudioRecording:
    Type: String
    Default: "true"
    AllowedValues:
      - "true"
      - "false"
    Description: >-
      Enable or disable recording of audio during a meeting. If set to false, the
      meeting will not be recorded.
      If set to true, the meeting will be recorded.

  AudioRecordingExpirationInDays:
    Type: Number
    Description:
      "Whole number of days after which audio recordings will be deleted
      from the Recordings bucket."
    Default: 90

  MeetingRecordExpirationInDays:
    Type: Number
    Default: 90
    Description: >-
      Number of days to retain meeting records. Meetings will be automatically deleted
      from LMA after this time.
      Specify partial days like 0.5 to retain meeting records for half a day.

  TranscriptionExpirationInDays:
    Type: Number
    Default: 90
    Description: >-
      Number of days to retain turn by turn meeting transcriptions.  Transcriptions
      will be automatically deleted from LMA after this time.
      Specify partial days like 0.5 to retain transcriptions for half a day.

  CloudWatchLogsExpirationInDays:
    Type: Number
    Default: 90
    AllowedValues:
      [
        1,
        3,
        5,
        7,
        14,
        30,
        60,
        90,
        120,
        150,
        180,
        365,
        400,
        545,
        731,
        1096,
        1827,
        2192,
        2557,
        2922,
        3288,
        3653,
      ]
    Description: >-
      Number of days to retain CloudWatch log records. Logs will be automatically
      deleted after this time.

  EnableAppSyncApiCache:
    Type: String
    Default: "false"
    AllowedValues:
      - "true"
      - "false"
    Description: >-
      Enable or disable the AppSync Api Cache.

  AppSyncApiCacheInstanceType:
    Type: String
    Default: SMALL
    AllowedValues:
      - SMALL
      - MEDIUM
      - LARGE
      - XLARGE
    Description: >-
      If you set the EnableAppSyncApiCache is true, choose a value that meets your workload requirements. 
      This parameter is ignored if the EnableAppSyncApiCache parameter is set to false (default behavior).

  UseExistingVPC:
    Type: String
    Default: "false"
    AllowedValues:
      - "true"
      - "false"
    Description: >-
      Use an existing VPC for the solution instead of creating a new one. If set to
      true, the VPC ID and subnets must be provided. DO NOT CHANGE VALUE WHEN UPDATING AN EXISTING STACK.

  VPC:
    Type: String
    Description: >-
      ID of the VPC to use for the solution. Required if UseExistingVPC is true. DO NOT CHANGE VALUE WHEN 
      UPDATING AN EXISTING STACK.
    AllowedPattern: "^(|vpc-[a-zA-Z0-9-]+)$"

  PublicSubnet1:
    Type: String
    Description: >-
      ID of the public subnet in Availability Zone 1. Required if UseExistingVPC is
      true. DO NOT CHANGE VALUE WHEN UPDATING AN EXISTING STACK.
    AllowedPattern: "^(|subnet-[a-zA-Z0-9-]+)$"

  PublicSubnet2:
    Type: String
    Description: >-
      ID of the public subnet in Availability Zone 2. Required if UseExistingVPC is
      true. DO NOT CHANGE VALUE WHEN UPDATING AN EXISTING STACK.
    AllowedPattern: "^(|subnet-[a-zA-Z0-9-]+)$"

  PrivateSubnet1:
    Type: String
    Description: >-
      ID of the private subnet in Availability Zone 1. Required if UseExistingVPC is
      true. DO NOT CHANGE VALUE WHEN UPDATING AN EXISTING STACK.
    AllowedPattern: "^(|subnet-[a-zA-Z0-9-]+)$"

  PrivateSubnet2:
    Type: String
    Description: >-
      ID of the private subnet in Availability Zone 2. Required if UseExistingVPC is
      true. DO NOT CHANGE VALUE WHEN UPDATING AN EXISTING STACK.
    AllowedPattern: "^(|subnet-[a-zA-Z0-9-]+)$"

  PermissionsBoundaryArn:
    Type: String
    Default: ""
    Description: >-
      (Optional) ARN of an existing IAM managed policy to use as permissions boundary for IAM roles.
      If provided, this policy will limit the maximum permissions that can be granted to IAM roles
      in this stack, providing an additional layer of security.

  EnableDataRetentionOnDelete:
    Type: String
    Default: "false"
    AllowedValues:
      - "true"
      - "false"
    Description: >-
      Enable data retention on stack deletion for compliance requirements.
      When true: DynamoDB tables, S3 buckets (recordings and logs), and encryption key will be retained.
      When false (default): All resources deleted for clean stack removal.
      Enterprise customers with compliance requirements should set to true.

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Web UI Authentication
        Parameters:
          - AdminEmail
          - AllowedSignUpEmailDomain
      - Label:
          default: Meeting Assist Options
        Parameters:
          - MeetingAssistService
          - MeetingAssistServiceBedrockModelID
          - TavilyApiKey
          - AssistantWakePhraseRegEx
          - MeetingAssistQnABotOpenSearchNodeCount
      - Label:
          default: Meeting Assist Bedrock Guardrails
        Parameters:
          - BedrockGuardrailId
          - BedrockGuardrailVersion
      - Label:
          default: Meeting Assist Bedrock Knowledge Base
        Parameters:
          - BedrockKnowledgeBaseId
          - BedrockKnowledgeBaseS3BucketName
          - BedrockKnowledgeBaseS3DocumentUploadFolderPrefix
          - BedrockKnowledgeBaseWebCrawlerURLs
          - BedrockKnowledgeBaseWebCrawlerScope
      - Label:
          default: Meeting Assist Bedrock Agent
        Parameters:
          - BedrockAgentId
          - BedrockAgentAliasId
      - Label:
          default: Meeting Assist Q Business
        Parameters:
          - AmazonQAppId
          - IDCApplicationARN
      - Label:
          default: Transcript Knowledge Base Configuration
        Parameters:
          - TranscriptKnowledgeBase
      - Label:
          default: Amazon S3 Configuration
        Parameters:
          - S3BucketName
          - AudioFilePrefix
          - TranscriptFilePrefix
      - Label:
          default: Amazon Transcribe Configuration
        Parameters:
          - IsPartialTranscriptEnabled
          - IsContentRedactionEnabled
          - TranscribeLanguageCode
          - TranscribeLanguageOptions
          - TranscribePreferredLanguage
          - TranscribeContentRedactionType
          - TranscribePiiEntityTypes
          - CustomVocabularyName
          - CustomLanguageModelName
      - Label:
          default: Transcript Event Processing Configuration
        Parameters:
          - TranscriptLambdaHookFunctionArn
          - TranscriptLambdaHookFunctionNonPartialOnly
          - EndOfCallTranscriptSummary
          - BedrockModelId
          - EndOfCallLambdaHookFunctionArn
          - StartOfCallLambdaHookFunctionArn
          - PostCallSummaryLambdaHookFunctionArn
      - Label:
          default: Download locations
      - Label:
          default: Amazon CloudFront Configuration
        Parameters:
          - CloudFrontPriceClass
          - CloudFrontAllowedGeos
      - Label:
          default: Retention Settings
        Parameters:
          - EnableAudioRecording
          - MeetingRecordExpirationInDays
          - TranscriptionExpirationInDays
          - AudioRecordingExpirationInDays
          - CloudWatchLogsExpirationInDays
          - EnableDataRetentionOnDelete
      - Label:
          default: AppSync Configuration
        Parameters:
          - EnableAppSyncApiCache
          - AppSyncApiCacheInstanceType
      - Label:
          default: VPC Configuration
        Parameters:
          - UseExistingVPC
          - VPC
          - PublicSubnet1
          - PublicSubnet2
          - PrivateSubnet1
          - PrivateSubnet2
      - Label:
          default: Security Configuration
        Parameters:
          - PermissionsBoundaryArn
      - Label:
          default: Business Domain
        Parameters:
          - Domain
      - Label:
          default: Workshop options
        Parameters:
          - ModelValidation
    ParameterLabels:
      CallAudioProcessor:
        default: Call Audio Processor
      ChimeVoiceToneAnalysis:
        default: Chime Voice Tone Analysis
      S3BucketName:
        default: Call Audio Recordings Bucket Name
      AudioFilePrefix:
        default: Audio File Prefix
      TranscriptFilePrefix:
        default: Transcript File Prefix
      CustomVoiceConnectorId:
        default: Amazon Chime SDK Voice Connector ID (existing)
      AdminEmail:
        default: Admin Email Address
      AllowedSignUpEmailDomain:
        default: Authorized Account Email Domain(s)
      MeetingAssistService:
        default: Meeting Assist Service
      MeetingAssistServiceBedrockModelID:
        default: Meeting Assist Service Bedrock Model ID
      TavilyApiKey:
        default: Tavily API Key for Web Search (STRANDS_BEDROCK only)
      AssistantWakePhraseRegEx:
        default: Meeting Assist Wake Phrase Regular Expression
      MeetingAssistQnABotOpenSearchNodeCount:
        default: Meeting Assist QnABot OpenSearch Node Count
      BedrockGuardrailId:
        default: Bedrock Guardrail Id (optional)
      BedrockGuardrailVersion:
        default: Bedrock Guardrail Version (optional)
      BedrockKnowledgeBaseId:
        default: Bedrock Knowledge Base Id (existing)
      BedrockAgentId:
        default: Bedrock Agent Id (existing)
      BedrockAgentAliasId:
        default: Bedrock Agent Alias Id (existing)
      BedrockKnowledgeBaseS3BucketName:
        default: S3 bucket with knowledge base source documents (optional).
      BedrockKnowledgeBaseS3DocumentUploadFolderPrefix:
        default: S3 prefix(es) for your content (optional)
      BedrockKnowledgeBaseWebCrawlerURLs:
        default: Publicly accessible URLs for web crawling (optional)
      BedrockKnowledgeBaseWebCrawlerScope:
        default: Web crawling sync scope
      AmazonQAppId:
        default: Amazon Q Application ID (existing)
      IDCApplicationARN:
        default: IDC Application ARN (existing)
      TranscriptKnowledgeBase:
        default: Transcript Knowledge Base
      IsPartialTranscriptEnabled:
        default: Enable Partial Transcripts
      IsContentRedactionEnabled:
        default: Enable Content Redaction for Transcripts
      TranscribeLanguageCode:
        default: Language for Transcription
      TranscribeLanguageOptions:
        default: Transcribe Language Options
      TranscribePreferredLanguage:
        default: Transcribe Preferred Language
      TranscribeContentRedactionType:
        default: Content Redaction Type for Transcription
      TranscribePiiEntityTypes:
        default: Transcription PII Redaction Entity Types
      CustomVocabularyName:
        default: Transcription Custom Vocabulary Name
      CustomLanguageModelName:
        default: Transcription Custom Language Model Name
      ModelValidation:
        default: Amazon Bedrock Model Validation
      TranscriptLambdaHookFunctionArn:
        default:
          Lambda Hook Function ARN for Custom Transcript Segment Processing
          (existing)
      TranscriptLambdaHookFunctionNonPartialOnly:
        default: Lambda Hook Function Mode Non-Partial only
      EndOfCallTranscriptSummary:
        default: End of Call Transcript Summary
      EndOfCallLambdaHookFunctionArn:
        default: Lambda Hook Function ARN for Custom End of Call Processing (existing)
      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)
      CloudFrontPriceClass:
        default: CloudFront Price Class
      CloudFrontAllowedGeos:
        default: CloudFront Allowed Geographies
      MeetingRecordExpirationInDays:
        default: Meeting Record Expiration In Days
      TranscriptionExpirationInDays:
        default: Transcription Expiration In Days
      AudioRecordingExpirationInDays:
        default: Audio Recording Expiration In Days
      CloudWatchLogsExpirationInDays:
        default: CloudWatch Logs Expiration In Days
      EnableAppSyncApiCache:
        default: Enable AppSync Api Cache
      AppSyncApiCacheInstanceType:
        default: AppSync Api Cache Instance Type
      UseExistingVPC:
        default: Use Existing VPC
      VPC:
        default: Existing VPC ID
      PublicSubnet1:
        default: Existing Public Subnet 1
      PublicSubnet2:
        default: Existing Public Subnet 2
      PrivateSubnet1:
        default: Existing Private Subnet 1
      PrivateSubnet2:
        default: Existing Private Subnet 2
      PermissionsBoundaryArn:
        default: Permissions Boundary ARN
      EnableDataRetentionOnDelete:
        default: Enable Data Retention On Stack Deletion

Mappings:
  TranscribeToLexLocaleId:
    identify-language:
      Value: en_US
    identify-multiple-languages:
      Value: en_US
    en-US:
      Value: en_US
    es-US:
      Value: es_US
    en-GB:
      Value: en_GB
    fr-CA:
      Value: fr_CA
    fr-FR:
      Value: fr_FR
    en-AU:
      Value: en_AU
    it-IT:
      Value: it_IT
    de-DE:
      Value: de_DE
    pt-BR:
      Value: pt_BR
    ja-JP:
      Value: ja_JP
    ko-KR:
      Value: ko_KR
    zh-CN:
      Value: zh_CH
    hi-IN:
      Value: hi_IN
    th-TH:
      Value: th_TH

Rules:
  # check required parameters
  BedrockKnowledgeBaseId:
    RuleCondition:
      !Equals [
        !Ref MeetingAssistService,
        "BEDROCK_KNOWLEDGE_BASE (Use Existing)",
      ]
    Assertions:
      - Assert: !Not [!Equals [!Ref BedrockKnowledgeBaseId, ""]]
        AssertDescription:
          BedrockKnowledgeBaseId is required when MeetingAssistService
          is 'BEDROCK_KNOWLEDGE_BASE (Use Existing)'
  BedrockAgentId:
    RuleCondition:
      !Equals [!Ref MeetingAssistService, "BEDROCK_AGENT (Use Existing)"]
    Assertions:
      - Assert: !Not [!Equals [!Ref BedrockAgentId, ""]]
        AssertDescription: BedrockAgentId is required when MeetingAssistService
          is 'BEDROCK_AGENT (Use Existing)'
  BedrockAgentAliasId:
    RuleCondition:
      !Equals [!Ref MeetingAssistService, "BEDROCK_AGENT (Use Existing)"]
    Assertions:
      - Assert: !Not [!Equals [!Ref BedrockAgentAliasId, ""]]
        AssertDescription:
          BedrockAgentAliasId is required when MeetingAssistService
          is 'BEDROCK_AGENT (Use Existing)'
  AmazonQId:
    RuleCondition:
      !Equals [!Ref MeetingAssistService, "Q_BUSINESS (Use Existing)"]
    Assertions:
      - Assert: !Not [!Equals [!Ref AmazonQAppId, ""]]
        AssertDescription: AmazonQAppId is required when MeetingAssistService
          is 'Q_BUSINESS (Use Existing)'

Conditions:
  ShouldRetainDataOnDelete: !Equals [!Ref EnableDataRetentionOnDelete, "true"]
  IsHealthcareDomainSelected: !Equals [!Ref Domain, "Healthcare"]
  ShouldCreateRecordingBucket: !Equals [!Ref S3BucketName, ""]
  ShouldUseExistingBedrockKnowledgeBase:
    !Equals [!Ref MeetingAssistService, "BEDROCK_KNOWLEDGE_BASE (Use Existing)"]
  ShouldCreateBedrockKnowledgeBase:
    !Or [
      !Equals [!Ref MeetingAssistService, "BEDROCK_KNOWLEDGE_BASE (Create)"],
      !Condition ShouldUseStrandsBedrockWithKB
    ]
  ShouldUseBedrockKnowledgeBase:
    !Or [
      !Condition ShouldUseExistingBedrockKnowledgeBase,
      !Condition ShouldCreateBedrockKnowledgeBase,
    ]
  ShouldUseExistingBedrockAgent:
    !Equals [!Ref MeetingAssistService, "BEDROCK_AGENT (Use Existing)"]
  ShouldCreateBedrockAgent:
    !Equals [
      !Ref MeetingAssistService,
      "BEDROCK_AGENT_WITH_KNOWLEDGE_BASE (Create)",
    ]
  ShouldUseBedrockAgent:
    !Or [
      !Condition ShouldUseExistingBedrockAgent,
      !Condition ShouldCreateBedrockAgent,
    ]
  ShouldUseQBusiness:
    !Equals [!Ref MeetingAssistService, "Q_BUSINESS (Use Existing)"]
  ShouldUseStrandsBedrock:
    !Equals [!Ref MeetingAssistService, "STRANDS_BEDROCK"]
  ShouldUseStrandsBedrockWithKB:
    !Equals [!Ref MeetingAssistService, "STRANDS_BEDROCK_WITH_KB (Create)"]
  ShouldUseAnyStrandsBedrock:
    !Or [
      !Condition ShouldUseStrandsBedrock,
      !Condition ShouldUseStrandsBedrockWithKB
    ]
  ShouldUseLLMOnly:
    !Not [
      !Or [
        !Condition ShouldUseBedrockKnowledgeBase,
        !Condition ShouldUseBedrockAgent,
        !Condition ShouldUseQBusiness,
        !Condition ShouldUseAnyStrandsBedrock,
      ],
    ]
  ShouldDeployQnABot:
    !Not [!Condition ShouldUseAnyStrandsBedrock]
  ShouldCreateVPC: !Equals [!Ref UseExistingVPC, "false"]
  ShouldCreateTranscriptKnowledgeBase:
    !Equals [!Ref TranscriptKnowledgeBase, "BEDROCK_KNOWLEDGE_BASE (Create)"]
  ShouldUseTranscriptKnowledgeBase: !Condition ShouldCreateTranscriptKnowledgeBase
  HasPermissionsBoundary: !Not [!Equals [!Ref PermissionsBoundaryArn, ""]]

Resources:
  ##########################################################################
  # Customer Managed KMS Key for encryption across all resources
  ##########################################################################
  CustomerManagedEncryptionKey:
    Type: AWS::KMS::Key
    DeletionPolicy: !If [ShouldRetainDataOnDelete, Retain, Delete]
    UpdateReplacePolicy: Retain
    Properties:
      Description: !Sub "Customer managed KMS key for ${AWS::StackName} encryption"
      EnableKeyRotation: true
      KeyPolicy:
        Version: "2012-10-17"
        Statement:
          - Sid: Enable IAM User Permissions
            Effect: Allow
            Principal:
              AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root"
            Action:
              - "kms:CreateAlias"
              - "kms:CreateGrant"
              - "kms:CreateKey"
              - "kms:DescribeKey"
              - "kms:EnableKey"
              - "kms:EnableKeyRotation"
              - "kms:ListAliases"
              - "kms:ListGrants"
              - "kms:ListKeys"
              - "kms:ListKeyPolicies"
              - "kms:ListResourceTags"
              - "kms:PutKeyPolicy"
              - "kms:UpdateAlias"
              - "kms:UpdateKeyDescription"
              - "kms:RevokeGrant"
              - "kms:DisableKey"
              - "kms:DisableKeyRotation"
              - "kms:GetKeyPolicy"
              - "kms:GetKeyRotationStatus"
              - "kms:DeleteAlias"
              - "kms:TagResource"
              - "kms:UntagResource"
              - "kms:ScheduleKeyDeletion"
              - "kms:CancelKeyDeletion"
              - "kms:Encrypt"
              - "kms:Decrypt"
              - "kms:ReEncryptFrom"
              - "kms:ReEncryptTo"
              - "kms:GenerateDataKey"
              - "kms:GenerateDataKeyWithoutPlaintext"
            Resource: "*"
          - Sid: Allow DynamoDB Service
            Effect: Allow
            Principal:
              Service: dynamodb.amazonaws.com
            Action:
              - kms:Encrypt
              - kms:Decrypt
              - kms:ReEncrypt*
              - kms:GenerateDataKey*
              - kms:DescribeKey
            Resource: "*"
            Condition:
              StringEquals:
                "aws:SourceAccount": !Ref "AWS::AccountId"
          - Sid: Allow CloudWatch Logs Service
            Effect: Allow
            Principal:
              Service: logs.amazonaws.com
            Action:
              - kms:Encrypt
              - kms:Decrypt
              - kms:ReEncrypt*
              - kms:GenerateDataKey*
              - kms:DescribeKey
            Resource: "*"
            Condition:
              StringEquals:
                "aws:SourceAccount": !Ref "AWS::AccountId"
          - Sid: Allow S3 Service
            Effect: Allow
            Principal:
              Service: s3.amazonaws.com
            Action:
              - kms:Encrypt
              - kms:Decrypt
              - kms:ReEncrypt*
              - kms:GenerateDataKey*
              - kms:DescribeKey
            Resource: "*"
            Condition:
              StringEquals:
                "aws:SourceAccount": !Ref "AWS::AccountId"
          - Sid: Allow SNS Service
            Effect: Allow
            Principal:
              Service: sns.amazonaws.com
            Action:
              - kms:Encrypt
              - kms:Decrypt
              - kms:ReEncrypt*
              - kms:GenerateDataKey*
              - kms:DescribeKey
            Resource: "*"
            Condition:
              StringEquals:
                "aws:SourceAccount": !Ref "AWS::AccountId"
          - Sid: Allow SQS Service
            Effect: Allow
            Principal:
              Service: sqs.amazonaws.com
            Action:
              - kms:Encrypt
              - kms:Decrypt
              - kms:ReEncrypt*
              - kms:GenerateDataKey*
              - kms:DescribeKey
            Resource: "*"
            Condition:
              StringEquals:
                "aws:SourceAccount": !Ref "AWS::AccountId"

  CustomerManagedEncryptionKeyAlias:
    Type: AWS::KMS::Alias
    Properties:
      AliasName: !Sub "alias/${AWS::StackName}-encryption-key"
      TargetKeyId: !Ref CustomerManagedEncryptionKey

  # EventBridge rule for KMS key monitoring
  KMSKeyMonitoringEventRule:
    Type: AWS::Events::Rule
    Properties:
      Name: !Sub "${AWS::StackName}-KMS-Key-Monitoring"
      Description: "EventBridge rule to monitor KMS key events for compliance and security"
      EventPattern:
        source:
          - "aws.kms"
        detail:
          eventSource:
            - "kms.amazonaws.com"
          eventName:
            - "DisableKey"
            - "ScheduleKeyDeletion"
            - "CancelKeyDeletion"
            - "CreateKey"
            - "PutKeyPolicy"
            - "EnableKey"
            - "DisableKeyRotation"
            - "EnableKeyRotation"
          resources:
            - !GetAtt CustomerManagedEncryptionKey.Arn
      State: ENABLED
      Targets:
        - Arn: !GetAtt KMSKeyMonitoringFunction.Arn
          Id: "KMSKeyMonitoringTarget"

  # Lambda function for KMS key monitoring
  KMSKeyMonitoringFunction:
    Type: AWS::Serverless::Function
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W89
            reason: Customer can use VPC if desired
          - id: W92
            reason: Customer can choose reserved concurrency based on their requirement.
    # checkov:skip=CKV_AWS_116: "DLQ not required for monitoring function"
    # checkov:skip=CKV_AWS_117: "Function does not require VPC access as it only interacts with AWS services via APIs"
    # checkov:skip=CKV_AWS_115: "Function does not require reserved concurrency as it scales based on demand"
    Properties:
      Handler: index.handler
      Runtime: python3.12
      Timeout: 60
      MemorySize: 128
      Description: "Lambda function to process KMS key events for monitoring and compliance"
      InlineCode: |
        import json
        import boto3
        import logging

        logger = logging.getLogger()
        logger.setLevel(logging.INFO)

        def handler(event, context):
            """
            Process KMS key events for monitoring and compliance.
            This function logs KMS key events and can be extended to send alerts.
            """
            try:
                logger.info(f"Received KMS event: {json.dumps(event)}")
                
                # Extract event details
                detail = event.get('detail', {})
                event_name = detail.get('eventName', 'Unknown')
                event_source = detail.get('eventSource', 'Unknown')
                source_ip = detail.get('sourceIPAddress', 'Unknown')
                user_identity = detail.get('userIdentity', {})
                
                # Log key information
                logger.info(f"KMS Event: {event_name}")
                logger.info(f"Source: {event_source}")
                logger.info(f"Source IP: {source_ip}")
                logger.info(f"User Identity: {json.dumps(user_identity)}")
                
                # Add custom logic here for specific event handling
                if event_name in ['DisableKey', 'ScheduleKeyDeletion']:
                    logger.warning(f"CRITICAL KMS EVENT: {event_name} - Key security may be compromised")
                elif event_name == 'PutKeyPolicy':
                    logger.info(f"KMS key policy updated - review for compliance")
                
                return {
                    'statusCode': 200,
                    'body': json.dumps({
                        'message': f'Successfully processed KMS event: {event_name}',
                        'eventProcessed': True
                    })
                }
                
            except Exception as e:
                logger.error(f"Error processing KMS event: {str(e)}")
                return {
                    'statusCode': 500,
                    'body': json.dumps({
                        'message': f'Error processing KMS event: {str(e)}',
                        'eventProcessed': False
                    })
                }
      LoggingConfig:
        LogGroup:
          Fn::Sub: /${AWS::StackName}/lambda/KMSKeyMonitoringFunction
    DependsOn:
      - KMSKeyMonitoringFunctionLogGroup

  KMSKeyMonitoringFunctionLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName:
        Fn::Sub: /${AWS::StackName}/lambda/KMSKeyMonitoringFunction
      RetentionInDays:
        Ref: CloudWatchLogsExpirationInDays
      KmsKeyId: !GetAtt CustomerManagedEncryptionKey.Arn

  # Permission for EventBridge to invoke the Lambda function
  KMSKeyMonitoringFunctionPermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref KMSKeyMonitoringFunction
      Action: lambda:InvokeFunction
      Principal: events.amazonaws.com
      SourceArn: !GetAtt KMSKeyMonitoringEventRule.Arn

  # Custom resource to enforce max length of StackName - prevent downstream failures
  StacknameCheckFunction:
    Type: AWS::Serverless::Function
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W89
            reason: Customer can use VPC if desired
          - id: W92
            reason: Customer can choose reserved concurrency based on their requirement.
    # checkov:skip=CKV_AWS_116: "DLQ not required for Cfn Custom Resource function"
    # checkov:skip=CKV_AWS_117: "Function does not require VPC access as it only interacts with AWS services via APIs"
    # checkov:skip=CKV_AWS_115: "Function does not require reserved concurrency as it scales based on demand"
    Properties:
      Handler: index.handler
      Runtime: python3.12
      InlineCode: |
        import cfnresponse
        import time
        import json
        def handler(event, context):
            print(json.dumps(event))
            input = event['ResourceProperties'].get('InputString', '')
            max_length = int(event['ResourceProperties'].get('MaxLength', 0))
            status = cfnresponse.SUCCESS
            reason = f"Stack Name Length under {max_length} - OK"
            if event['RequestType'] == "Create":
              if len(input) > max_length:
                status = cfnresponse.FAILED
                reason = f"Stack Name length too long - max length {max_length} - FAILED"
            else:
              print(f"Request type is {event['RequestType']} - skipping")
            cfnresponse.send(event, context, status, {}, reason=reason)
      LoggingConfig:
        LogGroup:
          Ref: StacknameCheckFunctionLogGroup
    DependsOn:
      - StacknameCheckFunctionLogGroup

  StacknameCheckFunctionLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      RetentionInDays:
        Ref: CloudWatchLogsExpirationInDays
      KmsKeyId: !GetAtt CustomerManagedEncryptionKey.Arn

  IsStacknameLengthOK:
    Type: Custom::StacknameCheck
    Properties:
      ServiceToken: !GetAtt StacknameCheckFunction.Arn
      InputString: !Ref "AWS::StackName"
      MaxLength: 25

  # Custom resource to validate parameters

  ValidateParametersFunctionRole:
    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
      PermissionsBoundary: !If
        - HasPermissionsBoundary
        - !Ref PermissionsBoundaryArn
        - !Ref AWS::NoValue
      Policies:
        - PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - "bedrock:InvokeModel"
                Resource: 
                  - !Sub "arn:aws:bedrock:*::foundation-model/*"
                  - !Sub "arn:aws:bedrock:${AWS::Region}:${AWS::AccountId}:inference-profile/*"
              - Effect: Allow
                Action:
                  - "bedrock:GetInferenceProfile"
                Resource:
                  - !Sub "arn:aws:bedrock:${AWS::Region}:${AWS::AccountId}:inference-profile/*"
          PolicyName: BedrockPolicy

  ValidateParametersFunction:
    Type: AWS::Serverless::Function
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W89
            reason: Customer can use VPC if desired
          - id: W92
            reason: Customer can choose reserved concurrency based on their requirement.
    # checkov:skip=CKV_AWS_116: "DLQ not required for Cfn Custom Resource function"
    # checkov:skip=CKV_AWS_117: "Function does not require VPC access as it only interacts with AWS services via APIs"
    # checkov:skip=CKV_AWS_115: "Function does not require reserved concurrency as it scales based on demand"
    Properties:
      Handler: index.handler
      Runtime: python3.12
      Role: !GetAtt ValidateParametersFunctionRole.Arn
      Timeout: 60
      InlineCode: |
        import cfnresponse
        import json
        import boto3
        from botocore.config import Config
        def is_model_available(modelId):
          try:
              client = boto3.client('bedrock-runtime', config=Config(retries={'max_attempts': 50, 'mode': 'adaptive'}))
              prompt = "Why is the sky blue?"
              if 'embed' in modelId:
                  body = {"inputText": prompt}
                  print(f"Testing model: {modelId} - {body}")
                  client.invoke_model(body=json.dumps(body), modelId=modelId, accept='application/json', contentType='application/json')
              else:
                  message = {
                    "role": "user",
                    "content": [{"text": prompt}]
                  }
                  print(f"Testing model: {modelId} - {message}")
                  client.converse(modelId=modelId, messages=[message])
              print(f"The model '{modelId}' is available and enabled.")
              return True
          except Exception as e:
              print(f"The model '{modelId}' is not available or not enabled. {e}")
              return False
        def handler(event, context):
            print(json.dumps(event))
            status = cfnresponse.SUCCESS
            reason = f"Parameters Validated"
            if event['RequestType'] != "Delete":
              # Validate Transcribe Language options
              transcribeLanguageCode = event['ResourceProperties'].get('TranscribeLanguageCode', '')
              transcribeLanguageOptions = event['ResourceProperties'].get('TranscribeLanguageOptions', '')
              transcribePreferredLanguage = event['ResourceProperties'].get('TranscribePreferredLanguage', '')
              customVocabularyName = event['ResourceProperties'].get('CustomVocabularyName', '')
              customLanguageModelName = event['ResourceProperties'].get('CustomLanguageModelName', '')
              modelValidation = event['ResourceProperties'].get('ModelValidation', '')
              if transcribeLanguageCode in ['identify-language', 'identify-multiple-languages']:
                if customVocabularyName:
                  status = cfnresponse.FAILED
                  reason = f"CustomVocabularyName cannot be set when TranscribeLanguageCode is '{transcribeLanguageCode}'"
                elif customLanguageModelName:
                  status = cfnresponse.FAILED
                  reason = f"CustomLanguageModelName cannot be set when TranscribeLanguageCode is '{transcribeLanguageCode}'"
                if transcribeLanguageOptions == '':
                  status = cfnresponse.FAILED
                  reason = f"At least two languages must be provided for TranscribeLanguageOptions when using language identification"
                else:
                  # only one language dialect per language
                  languages = set()
                  lang_codes = [l.strip() for l in transcribeLanguageOptions.split(',')]
                  for lang_code in lang_codes:
                    lang = lang_code.split('-')[0]
                    if lang in languages:
                      status = cfnresponse.FAILED
                      reason = f"TranscribeLanguageOptions has a duplicate entry for '{lang}' - only one language dialect per language is allowed."
                    languages.add(lang)
                  # transcribePreferredLanguage must be in transcribeLanguageOptions
                  if not transcribePreferredLanguage == 'None':
                    if not transcribePreferredLanguage in lang_codes:
                      status = cfnresponse.FAILED
                      reason = f"TranscribePreferredLanguage '{transcribePreferredLanguage}' is not in TranscribeLanguageOptions '{transcribeLanguageOptions}'"
              # Validate Bedrock model access
              failedModels = []
              requiredmodels = event['ResourceProperties'].get('BedrockModelIds', [])
              if modelValidation == "true":
                for modelId in requiredmodels: 
                  if not is_model_available(modelId):
                    failedModels.append(modelId)
                if failedModels:
                  status = cfnresponse.FAILED
                  reason = f"Model(s) '{failedModels}' not available or not enabled. Check that Amazon Bedrock is available in your region, and that required models ('{requiredmodels}') are activated in your Amazon Bedrock account"
            else:
              print(f"Request type is {event['RequestType']} - skipping")
            cfnresponse.send(event, context, status, {}, reason=reason)
      LoggingConfig:
        LogGroup:
          Ref: ValidateParametersFunctionLogGroup
    DependsOn:
      - ValidateParametersFunctionLogGroup

  ValidateParametersFunctionLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      RetentionInDays:
        Ref: CloudWatchLogsExpirationInDays
      KmsKeyId: !GetAtt CustomerManagedEncryptionKey.Arn

  ValidateParameters:
    Type: Custom::ValidateParameters
    Properties:
      ServiceToken: !GetAtt ValidateParametersFunction.Arn
      TranscribeLanguageCode: !Ref TranscribeLanguageCode
      TranscribeLanguageOptions: !Ref TranscribeLanguageOptions
      TranscribePreferredLanguage: !Ref TranscribePreferredLanguage
      CustomVocabularyName: !Ref CustomVocabularyName
      CustomLanguageModelName: !Ref CustomLanguageModelName
      ModelValidation: !Ref ModelValidation
      BedrockModelIds:
        - !Ref MeetingAssistServiceBedrockModelID # Meeting assistant
        - "amazon.titan-embed-text-v2:0" # Meeting Assistant Bedrock KB & QnABot
        - !Ref BedrockModelId # Meeting summarization

  ##########################################################################
  # Resources shared by multiple nested stacks
  ##########################################################################
  CallDataStream:
    Type: AWS::Kinesis::Stream
    Properties:
      RetentionPeriodHours: 24
      StreamModeDetails:
        StreamMode: ON_DEMAND
      StreamEncryption:
        EncryptionType: KMS
        KeyId: alias/aws/kinesis

  # Initialise LMASettingsParameter as empty (so it never gets replaced)
  # and use CR UpdateLMASettingsFunction to add/update values
  LMASettingsParameter:
    Type: AWS::SSM::Parameter
    Properties:
      Type: String
      Name: !Sub "${AWS::StackName}-LMASettings"
      Value: "{}"

  UpdateLMASettingsFunctionRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: "sts:AssumeRole"
            Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
        Version: 2012-10-17
      ManagedPolicyArns:
        - !Sub "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
      PermissionsBoundary: !If
        - HasPermissionsBoundary
        - !Ref PermissionsBoundaryArn
        - !Ref AWS::NoValue
      Policies:
        - PolicyName: updateSSMStore
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - ssm:GetParameter
                  - ssm:PutParameter
                Resource:
                  - !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${LMASettingsParameter}"

  UpdateLMASettingsFunction:
    Type: "AWS::Lambda::Function"
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W89
            reason: Customer can use VPC if desired
          - id: W92
            reason: Customer can choose reserved concurrency based on their requirement.
          - id: W58
            reason: Managed policy already provides access to CloudWatch logs.
    # checkov:skip=CKV_AWS_116: "DLQ not required for settings update function"
    # checkov:skip=CKV_AWS_117: "Function does not require VPC access as it only interacts with AWS services via APIs"
    # checkov:skip=CKV_AWS_115: "Function does not require reserved concurrency as it scales based on demand"
    Properties:
      Role: !GetAtt UpdateLMASettingsFunctionRole.Arn
      Handler: index.handler
      Runtime: python3.12
      Timeout: 900
      TracingConfig:
        Mode: Active
      Code:
        ZipFile: |
          import boto3
          import cfnresponse
          import json
          ssm = boto3.client('ssm')
          def updateSSMParameter(props):
            parameterName = props['LCASettingsName']
            parameterValues = props['LCASettingsKeyValuePairs']
            response = ssm.get_parameter(Name=parameterName)
            settingsJSON = response['Parameter']['Value']
            settings = json.loads(settingsJSON)
            print(f"Existing settings: {settings}")
            for key, value in parameterValues.items():
              settings[key] = value
            print(f"Updated settings: {settings}")
            newSettingsJSON = json.dumps(settings)
            ssm.put_parameter(Name=parameterName, Value=newSettingsJSON, Overwrite=True)
                        
          def handler(event, context):        
            print(json.dumps(event))
            status = cfnresponse.SUCCESS
            responseData = {}
            reason = "Success"
            props = event["ResourceProperties"]
            if event['RequestType'] != 'Delete':
              try:
                updateSSMParameter(props)
              except Exception as e:
                print(e)
                reason = f"Exception thrown: {e}"
                status = cfnresponse.FAILED              
            cfnresponse.send(event, context, status, responseData, reason=reason)
      LoggingConfig:
        LogGroup:
          Ref: UpdateLMASettingsFunctionLogGroup

  UpdateLMASettingsFunctionLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      RetentionInDays:
        Ref: CloudWatchLogsExpirationInDays
      KmsKeyId: !GetAtt CustomerManagedEncryptionKey.Arn

  UpdateLMASettingsValues:
    Type: AWS::CloudFormation::CustomResource
    Properties:
      ServiceToken: !GetAtt UpdateLMASettingsFunction.Arn
      LCASettingsName: !Ref LMASettingsParameter
      LCASettingsKeyValuePairs:
        AssistantWakePhraseRegEx: !Ref AssistantWakePhraseRegEx
        ShouldUseTranscriptKnowledgeBase: !If
        - ShouldUseTranscriptKnowledgeBase
        - True
        - False
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete

  ## LOGGING BUCKET FOR S3 ACCESS LOGS
  LoggingBucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: !If [ShouldRetainDataOnDelete, Retain, Delete]
    UpdateReplacePolicy: Retain
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W35
            reason: "This is the logging destination bucket - does not require its own access logging"
    # checkov:skip=CKV_AWS_18: "This is the logging destination bucket - does not require its own access logging"
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      OwnershipControls:
        Rules:
          - ObjectOwnership: BucketOwnerPreferred
      VersioningConfiguration:
        Status: Enabled
      LifecycleConfiguration:
        Rules:
          - Id: DeleteOldLogs
            Status: Enabled
            ExpirationInDays: 180
            Prefix: ""
            AbortIncompleteMultipartUpload:
              DaysAfterInitiation: 7

  LoggingBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref LoggingBucket
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: AllowS3ServerAccessLogs
            Effect: Allow
            Principal:
              Service: logging.s3.amazonaws.com
            Action:
              - s3:PutObject
            Resource: !Sub "${LoggingBucket.Arn}/*"
            Condition:
              StringEquals:
                "aws:SourceAccount": !Ref "AWS::AccountId"
          - Sid: EnforceSSLOnly
            Effect: Deny
            Principal: '*'
            Action: 's3:*'
            Resource: 
              - !Sub "${LoggingBucket.Arn}/*"
              - !Sub "${LoggingBucket.Arn}"
            Condition:
              Bool:
                'aws:SecureTransport': false

  ## BUCKET TO STORE STEREO RECORDINGS AND TRANSCRIPTS FILES
  RecordingsBucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: !If [ShouldRetainDataOnDelete, Retain, Delete]
    UpdateReplacePolicy: Retain
    Condition: ShouldCreateRecordingBucket
    Properties:
      AccessControl: LogDeliveryWrite
      OwnershipControls:
        Rules:
          - ObjectOwnership: ObjectWriter
      VersioningConfiguration:
        Status: Enabled
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      LoggingConfiguration:
        DestinationBucketName: !Ref LoggingBucket
        LogFilePrefix: "recordings-bucket-logs/"
      LifecycleConfiguration:
        Rules:
          - Id: AudioRetention
            Status: Enabled
            Prefix: !Ref AudioFilePrefix
            ExpirationInDays: !Ref AudioRecordingExpirationInDays
          - Id: TranscriptRetention
            Status: Enabled
            Prefix: !Ref TranscriptFilePrefix
            ExpirationInDays: !Ref TranscriptionExpirationInDays

  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

  ToJSONFunction:
    Type: AWS::Serverless::Function
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W89
            reason: Customer can use VPC if desired
          - id: W92
            reason: Customer can choose reserved concurrency based on their requirement.
    # checkov:skip=CKV_AWS_116: "DLQ not required for Cfn Custom Resource function"
    # checkov:skip=CKV_AWS_117: "Function does not require VPC access as it only interacts with AWS services via APIs"
    # checkov:skip=CKV_AWS_115: "Function does not require reserved concurrency as it scales based on demand"
    Properties:
      Handler: index.handler
      Runtime: python3.12
      InlineCode: |
        import cfnresponse
        import json
        def handler(event, context):
            print(json.dumps(event))
            status = cfnresponse.SUCCESS
            reason = "Sucessful"
            result = {}
            if event['RequestType'] in ["Create", "Update"]:
              output = {}
              for k, v in event['ResourceProperties'].items():
                if k != "ServiceToken":
                  output[k] = v
              outputJSON = json.dumps(output)
              result = {"Output": outputJSON}
            else:
              print(f"Request type is {event['RequestType']} - skipping")
            cfnresponse.send(event, context, status, result, reason=reason)
      LoggingConfig:
        LogGroup:
          Ref: ToJSONFunctionLogGroup
    DependsOn:
      - ToJSONFunctionLogGroup

  ToJSONFunctionLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      RetentionInDays:
        Ref: CloudWatchLogsExpirationInDays
      KmsKeyId: !GetAtt CustomerManagedEncryptionKey.Arn

  QNABOT:
    Type: AWS::CloudFormation::Stack
    Condition: ShouldDeployQnABot
    DependsOn:
      - IsStacknameLengthOK
      - ValidateParameters
    Properties:
      # yamllint disable rule:line-length
      TemplateURL: https://s3.ap-southeast-2.amazonaws.com/aws-bigdata-blog-replica-ap-southeast-2/artifacts/lma/0.2.21/aws-qnabot/templates/qnabot-main.json
      # yamllint enable rule:line-length
      Parameters:
        Email: !Ref AdminEmail
        Username: "Admin"
        OpenSearchNodeCount: !Ref MeetingAssistQnABotOpenSearchNodeCount
        OpenSearchFineGrainAccessControl: "FALSE"
        LexV2BotLocaleIds:
          !FindInMap [
            TranscribeToLexLocaleId,
            !Ref TranscribeLanguageCode,
            Value,
          ]
        BootstrapBucket: aws-bigdata-blog-replica-ap-southeast-2
        BootstrapPrefix: artifacts/lma/0.2.21/aws-qnabot
        InstallLexResponseBots: "false"
        EmbeddingsApi: "BEDROCK"
        EmbeddingsBedrockModelId: "amazon.titan-embed-text-v2"
        LLMApi: "DISABLED" # LMA configures its own KB/QBusiness LambdaHooks
        LogRetentionPeriod: !Ref CloudWatchLogsExpirationInDays

  LLMTEMPLATESTACK:
    Type: AWS::CloudFormation::Stack
    DependsOn:
      - IsStacknameLengthOK
      - ValidateParameters
    Properties:
      TemplateURL: https://s3.ap-southeast-2.amazonaws.com/aws-bigdata-blog-replica-ap-southeast-2/artifacts/lma/0.2.21/lma-llm-template-setup-stack/llm-template-setup.yaml
      Parameters:
        Domain: !Ref Domain
        CloudWatchLogsExpirationInDays: !Ref CloudWatchLogsExpirationInDays
        CustomerManagedEncryptionKeyArn: !GetAtt CustomerManagedEncryptionKey.Arn

  COGNITOSTACK:
    Type: AWS::CloudFormation::Stack
    DependsOn:
      - IsStacknameLengthOK
      - ValidateParameters
    Properties:
      # yamllint disable rule:line-length
      TemplateURL: https://s3.ap-southeast-2.amazonaws.com/aws-bigdata-blog-replica-ap-southeast-2/artifacts/lma/0.2.21/lma-cognito-stack/template.yaml
      # yamllint enable rule:line-length
      Parameters:
        LMAStackName: !Ref AWS::StackName
        S3BucketName: !If
          - ShouldCreateRecordingBucket
          - !Ref RecordingsBucket
          - !Ref S3BucketName
        AdminEmail: !Ref AdminEmail
        AllowedSignUpEmailDomain: !Ref AllowedSignUpEmailDomain
        LMASettingsParameterName: !Ref LMASettingsParameter
        CloudWatchLogsExpirationInDays: !Ref CloudWatchLogsExpirationInDays
        CustomerManagedEncryptionKeyArn: !GetAtt CustomerManagedEncryptionKey.Arn

  AISTACK:
    Type: AWS::CloudFormation::Stack
    DependsOn:
      - IsStacknameLengthOK
      - ValidateParameters
    Properties:
      # yamllint disable rule:line-length
      TemplateURL: https://s3.ap-southeast-2.amazonaws.com/aws-bigdata-blog-replica-ap-southeast-2/artifacts/lma/0.2.21/lma-ai-stack/0.2.21/template.yaml
      # yamllint enable rule:line-length
      Parameters:
        LMAStackName: !Ref AWS::StackName
        AllowedSignUpEmailDomain: !Ref AllowedSignUpEmailDomain
        AssistantWakePhraseRegEx: !Ref AssistantWakePhraseRegEx
        AudioRecordingExpirationInDays: !Ref AudioRecordingExpirationInDays
        EnableVoiceToneAnalysis: "false"
        CallAudioSource: "Genesys Cloud Audiohook Web Socket"
        S3BucketName: !If
          - ShouldCreateRecordingBucket
          - !Ref RecordingsBucket
          - !Ref S3BucketName
        AudioFilePrefix: !Ref AudioFilePrefix  
        TranscriptFilePrefix: !Ref TranscriptFilePrefix
        IsSentimentAnalysisEnabled: "false"
        CloudFrontPriceClass: !Ref CloudFrontPriceClass
        CloudFrontAllowedGeos: !Ref CloudFrontAllowedGeos
        IsLexAgentAssistEnabled: !If
          - ShouldUseAnyStrandsBedrock
          - false
          - true
        IsLambdaAgentAssistEnabled: !If
          - ShouldUseAnyStrandsBedrock
          - true
          - false
        AgentAssistExistingLambdaFunctionArn: !If
          - ShouldUseAnyStrandsBedrock
          - !Sub "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:STRANDS-${AWS::StackName}-MeetingAssist"
          - ""
        MeetingRecordExpirationInDays: !Ref MeetingRecordExpirationInDays
        TranscriptionExpirationInDays: !Ref TranscriptionExpirationInDays
        TranscriptLambdaHookFunctionArn: !Ref TranscriptLambdaHookFunctionArn
        TranscriptLambdaHookFunctionNonPartialOnly: !Ref TranscriptLambdaHookFunctionNonPartialOnly
        EndOfCallTranscriptSummary: !Ref EndOfCallTranscriptSummary
        BedrockModelId: !Ref BedrockModelId
        EndOfCallLambdaHookFunctionArn: !Ref EndOfCallLambdaHookFunctionArn
        StartOfCallLambdaHookFunctionArn: !Ref StartOfCallLambdaHookFunctionArn
        PostCallSummaryLambdaHookFunctionArn: !Ref PostCallSummaryLambdaHookFunctionArn
        LLMPromptTemplateTableName: !GetAtt LLMTEMPLATESTACK.Outputs.LLMPromptTemplateTableName
        CloudWatchLogsExpirationInDays: !Ref CloudWatchLogsExpirationInDays
        CallDataStream: !Ref CallDataStream
        CallDataStreamArn: !GetAtt CallDataStream.Arn
        LMASettingsParameterName: !Ref LMASettingsParameter
        EnableAppSyncApiCache: !Ref EnableAppSyncApiCache
        AppSyncApiCacheInstanceType: !Ref AppSyncApiCacheInstanceType
        UserPoolId: !GetAtt COGNITOSTACK.Outputs.UserPoolId
        CognitoClientId: !GetAtt COGNITOSTACK.Outputs.CognitoClientId
        AppIdentityPoolId: !GetAtt COGNITOSTACK.Outputs.AppIdentityPoolId
        UpdateLMASettingsFunctionArn: !GetAtt UpdateLMASettingsFunction.Arn
        CustomerManagedEncryptionKeyArn: !GetAtt CustomerManagedEncryptionKey.Arn
        LoggingBucketName: !Ref LoggingBucket
        EnableDataRetentionOnDelete: !Ref EnableDataRetentionOnDelete
        VPC: !If
          - ShouldCreateVPC
          - !GetAtt VPCSTACK.Outputs.VPC
          - !Ref VPC
        PublicSubnet1: !If
          - ShouldCreateVPC
          - !GetAtt VPCSTACK.Outputs.PublicSubnet1
          - !Ref PublicSubnet1
        PublicSubnet2: !If
          - ShouldCreateVPC
          - !GetAtt VPCSTACK.Outputs.PublicSubnet2
          - !Ref PublicSubnet2
        PermissionsBoundaryArn: !Ref PermissionsBoundaryArn
        BedrockKnowledgeBaseID: !If
          - ShouldUseTranscriptKnowledgeBase
          - !GetAtt TRANSCRIPTBEDROCKKB.Outputs.KnowledgeBaseID
          - ""

  BEDROCKKB:
    Type: AWS::CloudFormation::Stack
    Condition: ShouldCreateBedrockKnowledgeBase
    DependsOn:
      - IsStacknameLengthOK
      - ValidateParameters
    Properties:
      TemplateURL: https://s3.ap-southeast-2.amazonaws.com/aws-bigdata-blog-replica-ap-southeast-2/artifacts/lma/0.2.21/lma-bedrockkb-stack/template.yaml
      Parameters:
        pKnowledgeBaseBucketName: !Ref BedrockKnowledgeBaseS3BucketName
        pInputDocumentUploadFolderPrefix: !Ref BedrockKnowledgeBaseS3DocumentUploadFolderPrefix
        pS3SyncScheduleExpression: "cron(0 0 * * ? *)"  # default once a day at midnight
        pWebCrawlerURLs: !Ref BedrockKnowledgeBaseWebCrawlerURLs
        pWebCrawlerScope: !Ref BedrockKnowledgeBaseWebCrawlerScope
        pWebSyncScheduleExpression: "cron(0 0 * * ? *)"  # default once a day at midnight
        PermissionsBoundaryArn: !Ref PermissionsBoundaryArn

  BEDROCKAGENT:
    Type: AWS::CloudFormation::Stack
    Condition: ShouldCreateBedrockAgent
    DependsOn:
      - IsStacknameLengthOK
      - ValidateParameters
    Properties:
      TemplateURL: https://s3.ap-southeast-2.amazonaws.com/aws-bigdata-blog-replica-ap-southeast-2/artifacts/lma/0.2.21/lma-bedrockagent-stack/template.yaml
      Parameters:
        BedrockModelID: !Ref MeetingAssistServiceBedrockModelID
        BedrockKnowledgeBaseId: !Ref BedrockKnowledgeBaseId
        KnowledgeBaseBucketName: !Ref BedrockKnowledgeBaseS3BucketName
        InputDocumentUploadFolderPrefix: !Ref BedrockKnowledgeBaseS3DocumentUploadFolderPrefix
        WebCrawlerURLs: !Ref BedrockKnowledgeBaseWebCrawlerURLs
        WebCrawlerScope: !Ref BedrockKnowledgeBaseWebCrawlerScope
        SNSEmailAddress: !Ref AdminEmail
        CustomerManagedEncryptionKeyArn: !GetAtt CustomerManagedEncryptionKey.Arn
        LoggingBucketName: !Ref LoggingBucket
        PermissionsBoundaryArn: !Ref PermissionsBoundaryArn

  TRANSCRIPTBEDROCKKB:
    Type: AWS::CloudFormation::Stack
    Condition: ShouldCreateTranscriptKnowledgeBase
    DependsOn:
      - IsStacknameLengthOK
      - ValidateParameters
    Properties:
      TemplateURL: https://s3.ap-southeast-2.amazonaws.com/aws-bigdata-blog-replica-ap-southeast-2/artifacts/lma/0.2.21/lma-bedrockkb-stack/template.yaml
      Parameters:
        pKnowledgeBaseBucketName: !Ref RecordingsBucket
        pInputDocumentUploadFolderPrefix: !Ref TranscriptFilePrefix
        pS3SyncScheduleExpression: "cron(0/15 * ? * * *)"  # default is to sync transcripts every 15 minutes


  # Format nested JSON strings before nesting them in QnABotSettings in MEETINGASSISTSETUP
  SettingsJSON:
    Type: Custom::ToJSON
    Properties:
      ServiceToken: !GetAtt ToJSONFunction.Arn
      LLM_GENERATE_QUERY_ENABLE: "FALSE"
      LLM_GENERATE_QUERY_PROMPT_TEMPLATE: ""
      LLM_CHAT_HISTORY_MAX_MESSAGES: !If
        - ShouldUseBedrockKnowledgeBase
        - 1000 # knowledge base retrieveAndGenerate api has input text max length of 1000, so the transcript context needs to be truncated
        - !If
          - ShouldUseQBusiness
          - 1000 # Q Business ChatSync API has input text max length of 7000, so the transcript context needs to be truncated less aggressively
          - 1000 # Limits for Anthropic LLMs and Agents are much larger, but we'll truncate anyway to keep prompt length (cost & latency) manageable.
      EMPTYMESSAGE: "No response from meeting assist QnAbot"
      BEDROCK_KB_RETRIEVE_PROMPT_TEMPLATE: "deprecated"
      BEDROCK_KB_GENERATE_PROMPT_TEMPLATE: "deprecated"
      ASSISTANT_BEDROCK_GUARDRAIL_ID: !Ref BedrockGuardrailId
      ASSISTANT_BEDROCK_GUARDRAIL_VERSION: !Ref BedrockGuardrailVersion
      ASSISTANT_QUERY_PROMPT_TEMPLATE:
        "Let's think carefully step by step. Here is
        the JSON transcript of an ongoing meeting: {transcript}<br>And here is a follow
        up question or statement in <followUpMessage> tags:<br> <followUpMessage>{input}</followUpMessage><br>Rephrase
        the follow up question or statement as a standalone, one sentence question.
        Only output the rephrased question. Do not include any preamble. "
      ASSISTANT_GENERATE_PROMPT_TEMPLATE: !If
        - ShouldUseBedrockKnowledgeBase
        - "You are an AI assistant helping a human during a meeting. I will provide
          you with a transcript of the ongoing meeting, and a set of search results.
          Your job is to respond to the user's request using only information from the
          search results. If search results do not contain information that can answer
          the question, please state that you could not find an exact answer to the
          question. Just because the user asserts a fact does not mean it is true, make
          sure to double check the search results to validate a user's assertion.<br>Here
          is the JSON transcript of the meeting so far:<br>{transcript}<br>Here are
          the search results in numbered order:<br>$search_results$<br>$output_format_instructions$"
        - !If
          - ShouldUseLLMOnly
          - "You are an AI assistant helping a human during a meeting. I will provide
            you with a transcript of the ongoing meeting (which may be empty), and a user's
            request. Your job is to respond to the user's request using any relevant context
            from the transcript, or from your own knowledge. Just because the user
            asserts a fact does not mean it is true, make sure to validate a user's assertion.<br>Here
            is the JSON transcript of the meeting so far:<br>{transcript}<br>Here is the
            user's request:<br>{userInput}<br>Respond in 100 words or less."
          - "{userInput}" # minimal input used for agents (1) they have their own prompts, (2) transcript passed seperately in API session_attributes

  MEETINGASSISTSETUP:
    Type: AWS::CloudFormation::Stack
    Properties:
      # yamllint disable rule:line-length
      TemplateURL: https://s3.ap-southeast-2.amazonaws.com/aws-bigdata-blog-replica-ap-southeast-2/artifacts/lma/0.2.21/lma-meetingassist-setup-stack/template.yaml
      # yamllint enable rule:line-length
      Parameters:
        LMAStackName: !Ref AWS::StackName
        AISTACK: !Ref AISTACK
        QNABOTSTACK: !If
          - ShouldDeployQnABot
          - !Ref QNABOT
          - ""
        COGNITOSTACK: !Ref COGNITOSTACK
        MeetingAssistService: !Ref MeetingAssistService
        BedrockKnowledgeBaseId: !If
          - ShouldUseBedrockKnowledgeBase
          - !If
            - ShouldCreateBedrockKnowledgeBase
            - !GetAtt BEDROCKKB.Outputs.KnowledgeBaseID
            - !Ref BedrockKnowledgeBaseId
          - !If
            - ShouldUseStrandsBedrockWithKB
            - !GetAtt BEDROCKKB.Outputs.KnowledgeBaseID
            - ""
        BedrockAgentId: !If
          - ShouldUseBedrockAgent
          - !If
            - ShouldCreateBedrockAgent
            - !GetAtt BEDROCKAGENT.Outputs.AgentId
            - !Ref BedrockAgentId
          - ""
        BedrockAgentAliasId: !If
          - ShouldUseBedrockAgent
          - !If
            - ShouldCreateBedrockAgent
            - !GetAtt BEDROCKAGENT.Outputs.AgentAliasId
            - !Ref BedrockAgentAliasId
          - ""
        BedrockKnowledgeBaseS3BucketName: !Ref BedrockKnowledgeBaseS3BucketName
        AmazonQAppId: !If
          - ShouldUseQBusiness
          - !Ref AmazonQAppId
          - ""
        IDCApplicationARN: !Ref IDCApplicationARN
        MeetingAssistServiceBedrockModelID: !Ref MeetingAssistServiceBedrockModelID
        TavilyApiKey: !Ref TavilyApiKey
        LexMeetingAssistBotId: !If
          - ShouldDeployQnABot
          - !GetAtt QNABOT.Outputs.LexV2BotId
          - ""
        LexMeetingAssistAliasId: !If
          - ShouldDeployQnABot
          - !GetAtt QNABOT.Outputs.LexV2BotAliasId
          - ""
        LexMeetingAssistLocaleId: !FindInMap [
            TranscribeToLexLocaleId,
            !Ref TranscribeLanguageCode,
            Value,
          ]
        WebAppBucket: !GetAtt AISTACK.Outputs.WebAppBucket
        CloudFrontDistributionId: !GetAtt AISTACK.Outputs.CloudFrontDistributionId
        QnaMeetingAssistDemoJson: !If
          - IsHealthcareDomainSelected
          - aws-bigdata-blog-replica-ap-southeast-2/artifacts/lma/0.2.21/lma-meetingassist-setup-stack/qna-ma-healthcare-demo.jsonl
          - aws-bigdata-blog-replica-ap-southeast-2/artifacts/lma/0.2.21/lma-meetingassist-setup-stack/qna-ma-demo.jsonl
        TranscriptBedrockKnowledgeBaseId: !If
          - ShouldUseTranscriptKnowledgeBase
          - !GetAtt TRANSCRIPTBEDROCKKB.Outputs.KnowledgeBaseID
          - ""
        ModelValidation: !Ref ModelValidation
        CloudWatchLogsExpirationInDays: !Ref CloudWatchLogsExpirationInDays
        CustomerManagedEncryptionKeyArn: !GetAtt CustomerManagedEncryptionKey.Arn
        # Changes to Params below force AgentAssist Setup to update.
        TranscribeLanguageCode: !Ref TranscribeLanguageCode
        TranscriptLambdaHookFunctionArn: !Ref TranscriptLambdaHookFunctionArn
        TranscriptLambdaHookFunctionNonPartialOnly: !Ref TranscriptLambdaHookFunctionNonPartialOnly
        MeetingRecordExpirationInDays: !Ref MeetingRecordExpirationInDays
        TranscriptionExpirationInDays: !Ref TranscriptionExpirationInDays
        EndOfCallTranscriptSummary: !Ref EndOfCallTranscriptSummary
        EndOfCallLambdaHookFunctionArn: !Ref EndOfCallLambdaHookFunctionArn
        LexMeetingAssistIdentityPoolId: !GetAtt COGNITOSTACK.Outputs.LexAgentAssistIdentityPoolId
        LexMeetingAssistUserPoolClientId: !GetAtt COGNITOSTACK.Outputs.CognitoClientId
        LexMeetingAssistUserPoolId: !GetAtt COGNITOSTACK.Outputs.UserPoolId
        LexMeetingAssistDomainName: !GetAtt COGNITOSTACK.Outputs.CognitoLoginUrl
        CloudFrontDomainName: !GetAtt AISTACK.Outputs.CloudFrontDomainName
        AppSyncGraphQLUrl: !GetAtt AISTACK.Outputs.AppSyncGraphQLUrl
        FetchTranscriptFunctionArn: !GetAtt AISTACK.Outputs.FetchTranscriptArn
        TranscriptSummaryFunctionArn: !GetAtt AISTACK.Outputs.TranscriptSummaryFunctionArn
        # QnaBotSettings - JSON param with new settings values based on options selected
        QnaBotSettings: !GetAtt SettingsJSON.Output
        Version: v0.8.8

  VPCSTACK:
    Type: AWS::CloudFormation::Stack
    Condition: ShouldCreateVPC
    DependsOn:
      - IsStacknameLengthOK
      - ValidateParameters
    Properties:
      # yamllint disable rule:line-length
      TemplateURL: https://s3.ap-southeast-2.amazonaws.com/aws-bigdata-blog-replica-ap-southeast-2/artifacts/lma/0.2.21/lma-vpc-stack/template.yaml
      Parameters:
        LMAStackName: !Ref "AWS::StackName"
        CustomerManagedEncryptionKeyArn: !GetAtt CustomerManagedEncryptionKey.Arn

  WEBSOCKETTRANSCRIBERSTACK:
    Type: AWS::CloudFormation::Stack
    Properties:
      # yamllint disable rule:line-length
      TemplateURL: https://s3.ap-southeast-2.amazonaws.com/aws-bigdata-blog-replica-ap-southeast-2/artifacts/lma/0.2.21/lma-websocket-transcriber-stack/0.2.21/template.yaml
      # yamllint enable rule:line-length
      Parameters:
        S3BucketName: !If
          - ShouldCreateRecordingBucket
          - !Ref RecordingsBucket
          - !Ref S3BucketName
        AudioFilePrefix: !Ref AudioFilePrefix
        UserPoolId: !GetAtt COGNITOSTACK.Outputs.UserPoolId
        CallDataStreamName: !Ref CallDataStream
        CallDataStreamArn: !GetAtt CallDataStream.Arn
        CloudFrontPriceClass: !Ref CloudFrontPriceClass
        TranscribeApiMode: "standard"
        IsContentRedactionEnabled: !Ref IsContentRedactionEnabled
        TranscribeContentRedactionType: !Ref TranscribeContentRedactionType
        TranscribeLanguageCode: !Ref TranscribeLanguageCode
        TranscribeLanguageOptions: !Ref TranscribeLanguageOptions
        TranscribePreferredLanguage: !Ref TranscribePreferredLanguage
        TranscribePiiEntityTypes: !Ref TranscribePiiEntityTypes
        CustomVocabularyName: !Ref CustomVocabularyName
        CustomLanguageModelName: !Ref CustomLanguageModelName
        PcaS3BucketName: ""
        PcaTranscriptsPrefix: ""
        PcaPlaybackAudioFilePrefix: ""
        PcaWebAppURL: ""
        PcaWebAppCallPathPrefix: ""
        LMASettingsParameterName: !Ref LMASettingsParameter
        UpdateLMASettingsFunctionArn: !GetAtt UpdateLMASettingsFunction.Arn
        CloudWatchLogsExpirationInDays: !Ref CloudWatchLogsExpirationInDays
        EnableAudioRecording: !Ref EnableAudioRecording
        CustomerManagedEncryptionKeyArn: !GetAtt CustomerManagedEncryptionKey.Arn
        LoggingBucketName: !Ref LoggingBucket
        PermissionsBoundaryArn: !Ref PermissionsBoundaryArn
        VPC: !If
          - ShouldCreateVPC
          - !GetAtt VPCSTACK.Outputs.VPC
          - !Ref VPC
        PublicSubnet1: !If
          - ShouldCreateVPC
          - !GetAtt VPCSTACK.Outputs.PublicSubnet1
          - !Ref PublicSubnet1
        PublicSubnet2: !If
          - ShouldCreateVPC
          - !GetAtt VPCSTACK.Outputs.PublicSubnet2
          - !Ref PublicSubnet2
        PrivateSubnet1: !If
          - ShouldCreateVPC
          - !GetAtt VPCSTACK.Outputs.PrivateSubnet1
          - !Ref PrivateSubnet1
        PrivateSubnet2: !If
          - ShouldCreateVPC
          - !GetAtt VPCSTACK.Outputs.PrivateSubnet2
          - !Ref PrivateSubnet2

  VIRTUALPARTICIPANTSTACK:
    Type: AWS::CloudFormation::Stack
    DependsOn:
      - AISTACK
    Properties:
      # yamllint disable rule:line-length
      TemplateURL: https://s3.ap-southeast-2.amazonaws.com/aws-bigdata-blog-replica-ap-southeast-2/artifacts/lma/0.2.21/lma-virtual-participant-stack/template.yaml
      # yamllint enable rule:line-length
      Parameters:
        LMAStackName: !Ref AWS::StackName
        EnableAudioRecording: !Ref EnableAudioRecording
        S3BucketName: !If
          - ShouldCreateRecordingBucket
          - !Ref RecordingsBucket
          - !Ref S3BucketName
        AudioFilePrefix: !Ref AudioFilePrefix
        CallDataStreamName: !Ref CallDataStream
        TranscribeApiMode: "standard"
        IsContentRedactionEnabled: !Ref IsContentRedactionEnabled
        TranscribeContentRedactionType: !Ref TranscribeContentRedactionType
        TranscribeLanguageCode: !Ref TranscribeLanguageCode
        TranscribeLanguageOptions: !Ref TranscribeLanguageOptions
        TranscribePreferredLanguage: !Ref TranscribePreferredLanguage
        TranscribePiiEntityTypes: !Ref TranscribePiiEntityTypes
        CustomVocabularyName: !Ref CustomVocabularyName
        CustomLanguageModelName: !Ref CustomLanguageModelName
        SourceCodeLocation: aws-bigdata-blog-replica-ap-southeast-2/artifacts/lma/0.2.21/lma-virtual-participant-stack/src-3d67930364689fc1.zip
        LMASettingsParameterName: !Ref LMASettingsParameter
        UpdateLMASettingsFunctionArn: !GetAtt UpdateLMASettingsFunction.Arn
        CustomerManagedEncryptionKeyArn: !GetAtt CustomerManagedEncryptionKey.Arn
        GraphQLEndpoint: !GetAtt AISTACK.Outputs.AppSyncGraphQLUrl
        CloudFrontDomainName: !GetAtt AISTACK.Outputs.CloudFrontDomainName
        VPTaskRegistryTableName: !GetAtt AISTACK.Outputs.VPTaskRegistryTableName
        VNCTargetGroupArn: !GetAtt AISTACK.Outputs.VNCTargetGroupArn
        VNCALBListenerArn: !GetAtt AISTACK.Outputs.VNCALBListenerArn
        VPSecurityGroupId: !GetAtt AISTACK.Outputs.VPSecurityGroupId
        VirtualParticipantTableName: !GetAtt AISTACK.Outputs.VirtualParticipantTableName
        VirtualParticipantTableStreamArn: !GetAtt AISTACK.Outputs.VirtualParticipantTableStreamArn
        VPC: !If
          - ShouldCreateVPC
          - !GetAtt VPCSTACK.Outputs.VPC
          - !Ref VPC
        PublicSubnet1: !If
          - ShouldCreateVPC
          - !GetAtt VPCSTACK.Outputs.PublicSubnet1
          - !Ref PublicSubnet1
        PublicSubnet2: !If
          - ShouldCreateVPC
          - !GetAtt VPCSTACK.Outputs.PublicSubnet2
          - !Ref PublicSubnet2
        PrivateSubnet1: !If
          - ShouldCreateVPC
          - !GetAtt VPCSTACK.Outputs.PrivateSubnet1
          - !Ref PrivateSubnet1
        PrivateSubnet2: !If
          - ShouldCreateVPC
          - !GetAtt VPCSTACK.Outputs.PrivateSubnet2
          - !Ref PrivateSubnet2

  BROWSEREXTENSIONSTACK:
    Type: AWS::CloudFormation::Stack
    Properties:
      # yamllint disable rule:line-length
      TemplateURL: https://s3.ap-southeast-2.amazonaws.com/aws-bigdata-blog-replica-ap-southeast-2/artifacts/lma/0.2.21/lma-browser-extension-stack/template.yaml
      # yamllint enable rule:line-length
      Parameters:
        LMAStackName: !Ref AWS::StackName
        WebSocketEndpoint: !GetAtt WEBSOCKETTRANSCRIBERSTACK.Outputs.WebSocketEndpoint
        CognitoClientId: !GetAtt COGNITOSTACK.Outputs.CognitoClientId
        CognitoLoginUrl: !GetAtt COGNITOSTACK.Outputs.CognitoLoginUrl
        CloudfrontEndpoint: !GetAtt AISTACK.Outputs.CloudfrontEndpoint
        WebAppCloudFrontDistribution: !GetAtt AISTACK.Outputs.CloudFrontDistributionId
        WebAppBucket: !GetAtt AISTACK.Outputs.WebAppBucket
        SourceCodeLocation: aws-bigdata-blog-replica-ap-southeast-2/artifacts/lma/0.2.21/lma-browser-extension-stack/src-fd9fe514be61f62c.zip
        CloudWatchLogsExpirationInDays: !Ref CloudWatchLogsExpirationInDays
        CustomerManagedEncryptionKeyArn: !GetAtt CustomerManagedEncryptionKey.Arn

Outputs:
  ApplicationCloudfrontEndpoint:
    Description: LMA User Interface URL
    Value: !GetAtt AISTACK.Outputs.CloudfrontEndpoint

  ChromeExtensionDownloadUrl:
    Description: LMA Chrome Browser extension download link
    Value: !GetAtt BROWSEREXTENSIONSTACK.Outputs.ExtensionDownloadLocation

  LMAWebsocketEndpoint:
    Description: Websocket endpoint for websocket integration
    Value: !GetAtt WEBSOCKETTRANSCRIBERSTACK.Outputs.WebSocketEndpoint

  QnaBotContentDesigner:
    Description: Agent Assist Content Designer URL (QnABot on AWS)
    Value: !If
      - ShouldDeployQnABot
      - !GetAtt QNABOT.Outputs.ContentDesignerURL
      - "QnABot not deployed - STRANDS_BEDROCK selected"

  RecordingsS3Bucket:
    Description: Bucket which contains all the call recordings
    Value: !If
      - ShouldCreateRecordingBucket
      - !Ref RecordingsBucket
      - !Ref S3BucketName

  SNSCategoryTopicName:
    Description: The name of the SNS Topic for matched category and alert notifications
    Value: !GetAtt AISTACK.Outputs.SNSTopic

  FetchTranscriptLambdaArn:
    Description:
      The ARN of a Lambda function that will process and export a call
      transcript as a string.
    Value: !GetAtt AISTACK.Outputs.FetchTranscriptArn

  LLMDefaultPromptSummaryTemplate:
    Description:
      View the *default* summary prompts. Do not edit. To override default
      prompts, add or edit attributes in the custom summary prompts item.
    Value: !Sub "https://${AWS::Region}.console.aws.amazon.com/dynamodbv2/home?region=${AWS::Region}#edit-item?itemMode=2&pk=DefaultSummaryPromptTemplates&route=ROUTE_ITEM_EXPLORER&sk=&table=${LLMTEMPLATESTACK.Outputs.LLMPromptTemplateTableName}"

  LLMCustomPromptSummaryTemplate:
    Description:
      Edit the *custom* summary prompts. Attribute values here override
      the default summary prompts, and are preserved during stack updates.
    Value: !Sub "https://${AWS::Region}.console.aws.amazon.com/dynamodbv2/home?region=${AWS::Region}#edit-item?itemMode=2&pk=CustomSummaryPromptTemplates&route=ROUTE_ITEM_EXPLORER&sk=&table=${LLMTEMPLATESTACK.Outputs.LLMPromptTemplateTableName}"

  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

  LMASettingsParameterName:
    Description: Name of the Parameter store setting that holds the LMA Settings
    Value: !Ref LMASettingsParameter

  CognitoUserPoolTokenIssuerUrl:
    Description: Cognito User Pool Token Issuer URL (only used for QBusiness integration)
    Value: !Sub https://cognito-idp.${AWS::Region}.amazonaws.com/${COGNITOSTACK.Outputs.UserPoolId}

  CognitoUserPoolClientId:
    Description: Cognito User Pool Client Id (only used for QBusiness integration)
    Value: !GetAtt COGNITOSTACK.Outputs.CognitoClientId

  QBusinessLambdaHookFunctionRoleArn:
    Description: Function role for Lambda QBusiness plugin (only used for QBusiness integration)
    Value: !GetAtt MEETINGASSISTSETUP.Outputs.QBusinessLambdaHookFunctionRoleArn

  SNSTopicForAgentMessages:
    Value: !If
      - ShouldCreateBedrockAgent
      - !GetAtt BEDROCKAGENT.Outputs.SNSTopicForAgentMessages
      - "BEDROCK_AGENT_WITH_KNOWLEDGE_BASE (Create) option not selected"
    Description: Agent SendMessage SNS Topic ARN - Add subscriptions to this topic to receive messages from the LMA meeting assistant agent
