diff --git a/modules/ai-applications/README.md b/modules/ai-applications/README.md index c9abd12c1..1996f9fee 100644 --- a/modules/ai-applications/README.md +++ b/modules/ai-applications/README.md @@ -20,7 +20,10 @@ This module handles the creation of [AI Applications](https://cloud.google.com/g ## APIs -This module uses the API `discoveryengine.googleapis.com` +This module uses these APIs + +- `discoveryengine.googleapis.com` +- `dialogflow.googleapis.com` (if you create a chat engine) ## Quota Project @@ -35,7 +38,8 @@ export USER_PROJECT_OVERRIDE=true ### Chat Engine -This is a minimal example to create a Chat Engine agent. +This is a minimal example to create a Chat Engine (Dialogflow CX) agent. +By default, this uses the location `global` for engines, agents and data stores. ```hcl module "ai-applications" { @@ -48,17 +52,160 @@ module "ai-applications" { } } engines_configs = { - my-chat-engine = { - data_store_ids = ["data-store-1"] - chat_engine_config = { - company_name = "Google" - default_language_code = "en" - time_zone = "America/Los_Angeles" + data_store_ids = ["data-store-1"] + chat_engine_config = { + company_name = "Google" + default_language_code = "en" + time_zone = "America/Los_Angeles" + } + } +} +# tftest modules=1 resources=3 +``` + +You can change this location for all components. + +```hcl +module "ai-applications" { + source = "./fabric/modules/ai-applications" + name = "my-chat-app" + project_id = var.project_id + location = "eu" + data_stores_configs = { + data-store-1 = { + solution_types = ["SOLUTION_TYPE_CHAT"] + } + } + engines_configs = { + data_store_ids = ["data-store-1"] + chat_engine_config = { + company_name = "Google" + default_language_code = "en" + time_zone = "America/Los_Angeles" + } + } +} +# tftest modules=1 resources=3 +``` + +You may need to create the Dialogflow CX agent in a specific region. +While the agent can be created within a specific region, the engine and the data stores still need to be created in multi-regional locations. Refer to [this table](https://docs.cloud.google.com/dialogflow/cx/docs/concept/region#avail) for the compatibility matrix. +In this case, you need to specify different locations for each component. + +```hcl +module "ai-applications" { + source = "./fabric/modules/ai-applications" + name = "my-chat-app" + project_id = var.project_id + data_stores_configs = { + data-store-1 = { + solution_types = ["SOLUTION_TYPE_CHAT"] + } + } + engines_configs = { + data_store_ids = ["data-store-1"] + location = "eu" + chat_engine_config = { + company_name = "Google" + default_language_code = "en" + time_zone = "America/Los_Angeles" + agent_config = { + location = "europe-west1" } } } } -# tftest modules=1 resources=2 +# tftest modules=1 resources=3 +``` + +Instead of creating a new agent, you can reference an existing agent. + +```hcl +module "ai-applications" { + source = "./fabric/modules/ai-applications" + name = "my-chat-app" + project_id = var.project_id + data_stores_configs = { + data-store-1 = { + solution_types = ["SOLUTION_TYPE_CHAT"] + } + } + engines_configs = { + data_store_ids = ["data-store-1"] + chat_engine_config = { + company_name = "Google" + default_language_code = "en" + time_zone = "America/Los_Angeles" + agent_config = { + security_settings_config = { + id = "projects/my-project/locations/global/agents/my-agent" + } + } + } + } +} +# tftest modules=1 resources=3 +``` + +If you create and agent, you can also create the agent security settings. + +```hcl +module "ai-applications" { + source = "./fabric/modules/ai-applications" + name = "my-chat-app" + project_id = var.project_id + data_stores_configs = { + data-store-1 = { + solution_types = ["SOLUTION_TYPE_CHAT"] + } + } + engines_configs = { + data_store_ids = ["data-store-1"] + chat_engine_config = { + company_name = "Google" + default_language_code = "en" + time_zone = "America/Los_Angeles" + agent_config = { + security_settings_config = { + create = true + } + } + } + } +} +# tftest modules=1 resources=4 +``` + +With the `security_settings_config` you can control every security aspect of the agent, including the creation of the DLP inspect and deidentify templates. + +You can also reference an existing security profile by passing its id. + +```hcl +module "ai-applications" { + source = "./fabric/modules/ai-applications" + name = "my-chat-app" + project_id = var.project_id + data_stores_configs = { + data-store-1 = { + solution_types = ["SOLUTION_TYPE_CHAT"] + } + } + engines_configs = { + data_store_ids = ["data-store-1"] + chat_engine_config = { + company_name = "Google" + default_language_code = "en" + time_zone = "America/Los_Angeles" + agent_config = { + security_settings_config = { + create = false + id = "projects/my-project/locations/global/securitySettings/my-sec-settings" + } + } + } + } +} +# tftest modules=1 resources=3 ``` ### Search Engine @@ -76,69 +223,17 @@ module "ai-applications" { } } engines_configs = { - my-search-engine = { - data_store_ids = ["data-store-1"] - search_engine_config = {} - } + data_store_ids = ["data-store-1"] + search_engine_config = {} } } # tftest modules=1 resources=2 ``` -### Deploy your service into a region - -By default services are deployed globally. You optionally specify a region where to deploy them. - -```hcl -module "ai-applications" { - source = "./fabric/modules/ai-applications" - name = "my-chat-app" - project_id = var.project_id - location = var.region - data_stores_configs = { - data-store-1 = { - solution_types = ["SOLUTION_TYPE_CHAT"] - } - } - engines_configs = { - my-chat-engine = { - data_store_ids = ["data-store-1"] - chat_engine_config = { - company_name = "Google" - default_language_code = "en" - time_zone = "America/Los_Angeles" - } - } - } -} -# tftest modules=1 resources=2 -``` - -### Reference existing data sources - -You can reference from engines existing data sources created outside this module, by passing their ids. In this case, you'll need to configure in the engine valid `industry_vertical` and `location`. - -```hcl -module "ai-applications" { - source = "./fabric/modules/ai-applications" - name = "my-search-app" - project_id = var.project_id - engines_configs = { - my-search-engine = { - data_store_ids = [ - "projects/my-project/locations/global/collections/my-collection/dataStores/data-store-1" - ] - industry_vertical = "GENERIC" - search_engine_config = {} - } - } -} -# tftest modules=1 resources=1 -``` - -### Using multiple data stores +### Data stores You can create and connect from your engines multiple data stores. +Data stores can be either created in the module or you can reference existing data stores, by passing their id. ```hcl module "ai-applications" { @@ -154,26 +249,24 @@ module "ai-applications" { } } engines_configs = { - my-chat-engine = { - data_store_ids = [ - "data-store-1", - "data-store-2", - "projects/my-project/locations/global/collections/default_collection/dataStores/data-store-3" - ] - chat_engine_config = { - company_name = "Google" - default_language_code = "en" - time_zone = "America/Los_Angeles" - } + data_store_ids = [ + "data-store-1", + "data-store-2", + "projects/my-project/locations/global/collections/default_collection/dataStores/data-store-3" + ] + chat_engine_config = { + company_name = "Google" + default_language_code = "en" + time_zone = "America/Los_Angeles" } } } -# tftest modules=1 resources=3 +# tftest modules=1 resources=4 ``` ### Set data store schemas -You can configure JSON data store schema definitions directly in your data store configuration. +You can configure JSON data store schemas directly in your data store configuration. ```hcl module "ai-applications" { @@ -192,7 +285,7 @@ module "ai-applications" { ### Back data stores with websites data -You can make data stores point to multiple websites and optionally specify their sitemap. +For search engines, you can make data stores point to multiple websites and optionally specify their sitemap. ```hcl module "ai-applications" { @@ -218,13 +311,11 @@ module "ai-applications" { } } engines_configs = { - my-search-engine = { - data_store_ids = [ - "website-search-ds" - ] - industry_vertical = "GENERIC" - search_engine_config = {} - } + data_store_ids = [ + "website-search-ds" + ] + industry_vertical = "GENERIC" + search_engine_config = {} } } # tftest modules=1 resources=5 @@ -234,11 +325,12 @@ module "ai-applications" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L159) | The name of the resources. | string | ✓ | | -| [project_id](variables.tf#L165) | The ID of the project where the data stores and the agents will be created. | string | ✓ | | -| [data_stores_configs](variables.tf#L17) | The ai-applications datastore configurations. | map(object({…})) | | {} | -| [engines_configs](variables.tf#L112) | The ai-applications engines configurations. | map(object({…})) | | {} | -| [location](variables.tf#L153) | Location where the data stores and agents will be created. | string | | "global" | +| [name](variables.tf#L483) | The name of the resources. | string | ✓ | | +| [project_id](variables.tf#L489) | The ID of the project where the data stores and the agents will be created. | string | ✓ | | +| [chat_agent_security_configs](variables.tf#L17) | The DLP security configurations for (Dialogflow CX) chat agents. | object({…}) | | {} | +| [data_stores_configs](variables.tf#L305) | The ai-applications datastore configurations. | map(object({…})) | | {} | +| [engines_configs](variables.tf#L410) | The AI applications engines configurations. | object({…}) | | {} | +| [location](variables.tf#L477) | Location where the data stores and agents will be created. | string | | "global" | ## Outputs diff --git a/modules/ai-applications/chat_agent.tf b/modules/ai-applications/chat_agent.tf new file mode 100644 index 000000000..f4bed0bd4 --- /dev/null +++ b/modules/ai-applications/chat_agent.tf @@ -0,0 +1,695 @@ +/** + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +resource "google_dialogflow_cx_agent" "default" { + count = ( + var.engines_configs.chat_engine_config != null + && try(var.engines_configs.chat_engine_config.agent_config.id, null) == null + ? 1 : 0 + ) + display_name = var.name + project = var.project_id + description = var.engines_configs.chat_engine_config.agent_config.description + default_language_code = coalesce(var.engines_configs.chat_engine_config.agent_config.default_language_code, "en") + time_zone = coalesce(var.engines_configs.chat_engine_config.agent_config.time_zone, "America/Los_Angeles") + supported_language_codes = var.engines_configs.chat_engine_config.agent_config.supported_language_codes + avatar_uri = var.engines_configs.chat_engine_config.agent_config.avatar_uri + location = coalesce( + try(var.engines_configs.chat_engine_config.agent_config.location, null), + try(google_discovery_engine_data_store.default[var.engines_configs.data_store_ids[0]].location, null), + var.location + ) + security_settings = try( + coalesce( + try(var.engines_configs.chat_engine_config.agent_config.security_settings_config.id, null), + try(google_dialogflow_cx_security_settings.default[0].id, null) + ), + null + ) +} + +resource "google_dialogflow_cx_security_settings" "default" { + count = ( + try(var.engines_configs.chat_engine_config.agent_config.security_settings_config.id == null) + && try(var.engines_configs.chat_engine_config.agent_config.security_settings_config.create, false) + ? 1 : 0 + ) + display_name = var.name + project = var.project_id + location = coalesce(var.chat_agent_security_configs.location, var.location) + redaction_strategy = var.chat_agent_security_configs.redaction_strategy + redaction_scope = var.chat_agent_security_configs.redaction_scope + inspect_template = try(google_data_loss_prevention_inspect_template.default[0].id, null) + deidentify_template = try(google_data_loss_prevention_deidentify_template.default[0].id, null) + purge_data_types = var.chat_agent_security_configs.purge_data_types + retention_window_days = var.chat_agent_security_configs.retention_window_days + + dynamic "audio_export_settings" { + for_each = ( + var.chat_agent_security_configs.audio_export_settings == null + ? {} : { 1 = 1 } + ) + + content { + gcs_bucket = ( + google_storage_bucket.bucket.id + ) + audio_export_pattern = var.chat_agent_security_configs.audio_export_settings.audio_export_pattern + enable_audio_redaction = var.chat_agent_security_configs.audio_export_settings.enable_audio_redaction + audio_format = var.chat_agent_security_configs.audio_export_settings.audio_format + } + } + + dynamic "insights_export_settings" { + for_each = ( + var.chat_agent_security_configs.enable_insights_export == null + || var.chat_agent_security_configs.enable_insights_export == false + ? {} : { 1 = 1 } + ) + + content { + enable_insights_export = true + } + } +} + +module "audio_export_settings_bucket" { + count = ( + var.chat_agent_security_configs.audio_export_settings == null + || try(var.chat_agent_security_configs.audio_export_settings.id, null) != null + ) ? 0 : 1 + source = "../gcs" + project_id = var.project_id + prefix = var.chat_agent_security_configs.audio_export_settings.gcs_bucket_config.prefix + name = try( + var.chat_agent_security_configs.audio_export_settings.gcs_bucket_config.prefix, + var.name + ) + location = coalesce( + try(var.chat_agent_security_configs.audio_export_settings.gcs_bucket_config.location), + var.location + ) + versioning = var.chat_agent_security_configs.audio_export_settings.gcs_bucket_config.versioning +} + +resource "google_data_loss_prevention_inspect_template" "default" { + count = ( + try(var.engines_configs.chat_engine_config.agent_config.security_settings_config.dlp_inspect_template, null) == null + ? 0 : 1 + ) + template_id = var.chat_agent_security_configs.dlp_inspect_template.template_id + display_name = var.name + description = var.chat_agent_security_configs.dlp_inspect_template.description + parent = coalesce( + var.chat_agent_security_configs.dlp_inspect_template.parent, + "projects/${var.project_id}" + ) + + inspect_config { + exclude_info_types = var.chat_agent_security_configs.dlp_inspect_template.exclude_info_types + include_quote = var.chat_agent_security_configs.dlp_inspect_template.include_quote + min_likelihood = var.chat_agent_security_configs.dlp_inspect_template.min_likelihood + content_options = var.chat_agent_security_configs.dlp_inspect_template.content_options + + dynamic "info_types" { + for_each = try(var.chat_agent_security_configs.dlp_inspect_template.custom_info_types, {}) + + content { + name = info_types.key + version = info_types.value.version + + dynamic "sensitivity_score" { + for_each = info_types.value.sensitivity_score == null ? [] : [1] + + content { + score = info_types.value.sensitivity_score + } + } + } + } + + dynamic "custom_info_types" { + for_each = ( + try(var.chat_agent_security_configs.dlp_inspect_template.info_types, + {}) + ) + iterator = info_types + + content { + exclusion_type = info_types.value.exclusion_type + likelihood = info_types.value.likelihood + + dynamic "surrogate_type" { + for_each = info_types.value.surrogate_type == null ? {} : { 1 = 1 } + + content {} + } + + info_type { + name = info_types.key + version = info_types.value.version + + dynamic "sensitivity_score" { + for_each = ( + info_types.value.sensitivity_score == null + ? {} : { 1 = 1 } + ) + + content { + score = info_types.value.sensitivity_score + } + } + } + + dynamic "regex" { + for_each = info_types.value.regex == null ? {} : { 1 = 1 } + + content { + group_indexes = info_types.value.regex.group_indexes + pattern = info_types.value.regex.pattern + } + } + + dynamic "sensitivity_score" { + for_each = info_types.value.sensitivity_score == null ? {} : { 1 = 1 } + + content { + score = info_types.value.sensitivity_score + } + } + + dynamic "stored_type" { + for_each = info_types.value.stored_type_name == null ? {} : { 1 = 1 } + + content { + name = info_types.value.stored_type_name + } + } + + dynamic "dictionary" { + for_each = info_types.value.dictionary == null ? {} : { 1 = 1 } + + content { + + dynamic "cloud_storage_path" { + for_each = ( + info_types.value.dictionary.cloud_storage_path == null + ? {} : { 1 = 1 } + ) + + content { + path = info_types.value.dictionary.cloud_storage_path + } + } + + dynamic "word_list" { + for_each = ( + info_types.value.dictionary.word_list == null ? {} : { 1 = 1 } + ) + + content { + words = info_types.value.dictionary.word_list + } + } + } + } + } + } + + dynamic "limits" { + for_each = ( + var.chat_agent_security_configs.dlp_inspect_template.limits == null + ? [] : [var.chat_agent_security_configs.dlp_inspect_template.limits] + ) + + content { + max_findings_per_item = limits.value.max_findings_per_item + max_findings_per_request = limits.value.max_findings_per_request + + dynamic "max_findings_per_info_type" { + for_each = limits.value.max_findings_per_info_type + + content { + max_findings = max_findings_per_info_type.value.max_findings + + info_type { + name = max_findings_per_info_type.key + version = max_findings_per_info_type.value.version + + dynamic "sensitivity_score" { + for_each = ( + max_findings_per_info_type.value.sensitivity_score == null + ? {} : { 1 = 1 } + ) + + content { + score = max_findings_per_info_type.value.sensitivity_score + } + } + } + } + } + } + } + + dynamic "rule_set" { + for_each = ( + var.chat_agent_security_configs.dlp_inspect_template.rule_sets + ) + + content { + dynamic "info_types" { + for_each = rule_set.value.info_types + + content { + name = info_types.key + version = info_types.value.version + + dynamic "sensitivity_score" { + for_each = ( + info_types.value.sensitivity_score == null + ? {} : { 1 = 1 } + ) + + content { + score = info_types.value.sensitivity_score + } + } + } + } + + rules { + dynamic "exclusion_rule" { + for_each = ( + rule_set.value.rules.exclusion_rule == null + ? {} : { 1 = 1 } + ) + + content { + matching_type = rule_set.value.rules.exclusion_rule.matching_type + + dynamic "dictionary" { + for_each = ( + rule_set.value.rules.exclusion_rule.dictionary == null + ? {} : { 1 = 1 } + ) + + content { + dynamic "cloud_storage_path" { + for_each = ( + rule_set.value.rules.exclusion_rule.dictionary.cloud_storage_path == null + ? {} : { 1 = 1 } + ) + + content { + path = rule_set.value.rules.exclusion_rule.dictionary.cloud_storage_path + } + } + dynamic "word_list" { + for_each = ( + rule_set.value.rules.exclusion_rule.dictionary.words_list == null + ? {} : { 1 = 1 } + ) + + content { + words = rule_set.value.rules.exclusion_rule.dictionary.words_list + } + } + } + } + + dynamic "regex" { + for_each = ( + rule_set.value.rules.exclusion_rule.regex == null + ? {} : { 1 = 1 } + ) + + content { + pattern = rule_set.value.rules.exclusion_rule.regex.pattern + group_indexes = rule_set.value.rules.exclusion_rule.regex.group_indexes + } + } + } + } + + dynamic "hotword_rule" { + for_each = ( + rule_set.value.rules.hotword_rule == null + ? {} : { 1 = 1 } + ) + + content { + dynamic "hotword_regex" { + for_each = ( + rule_set.value.rules.hotword_rule.hotword_regex == null + ? {} : { 1 = 1 } + ) + + content { + pattern = rule_set.value.rules.hotword_rule.hotword_regex.pattern + group_indexes = rule_set.value.rules.hotword_rule.hotword_regex.group_indexes + } + } + + dynamic "proximity" { + for_each = ( + rule_set.value.rules.hotword_rule.proximity == null + ? {} : { 1 = 1 } + ) + + content { + window_after = rule_set.value.rules.hotword_rule.proximity.window_after + window_before = rule_set.value.rules.hotword_rule.proximity.window_before + } + } + + dynamic "likelihood_adjustment" { + for_each = ( + rule_set.value.rules.hotword_rule.likelihood_adjustment == null + ? [{ fixed_likelihood = "VERY_LIKELY" }] + : [rule_set.value.rules.hotword_rule.likelihood_adjustment] + ) + + content { + fixed_likelihood = likelihood_adjustment.value.fixed_likelihood + relative_likelihood = likelihood_adjustment.value.relative_likelihood + } + } + } + } + } + } + } + } +} + +resource "google_data_loss_prevention_deidentify_template" "default" { + count = ( + try(var.engines_configs.chat_engine_config.agent_config.security_settings_config.dlp_deidentify_template, null) == null + ? 0 : 1 + ) + parent = coalesce( + var.chat_agent_security_configs.dlp_deidentify_template.parent, + "projects/${var.project_id}" + ) + display_name = var.name + description = var.chat_agent_security_configs.dlp_deidentify_template.description + template_id = var.chat_agent_security_configs.dlp_deidentify_template.template_id + + dynamic "deidentify_config" { + for_each = ( + var.chat_agent_security_configs.dlp_deidentify_template == null + ? [] : [var.chat_agent_security_configs.dlp_deidentify_template] + ) + + content { + dynamic "image_transformations" { + for_each = ( + deidentify_config.value.image_transformations == null + ? [] : [deidentify_config.value.image_transformations] + ) + + content { + dynamic "transforms" { + for_each = image_transformations.value.transforms + + content { + dynamic "all_info_types" { + for_each = transforms.value.all_info_types ? { 1 = 1 } : {} + + content {} + } + + dynamic "all_text" { + for_each = ( + transforms.value.all_text ? { 1 = 1 } : {} + ) + + content {} + } + + dynamic "redaction_color" { + for_each = ( + transforms.value.redaction_color == null + ? [] : [transforms.value.redaction_color] + ) + + content { + blue = redaction_color.value.blue + green = redaction_color.value.green + red = redaction_color.value.red + } + } + + dynamic "selected_info_types" { + for_each = ( + transforms.value.selected_info_types == null + ? [] : [transforms.value.selected_info_types] + ) + + content { + dynamic "info_types" { + for_each = selected_info_types.value.info_types + + content { + name = info_types.value.name + version = info_types.value.version + + dynamic "sensitivity_score" { + for_each = ( + info_types.value.sensitivity_score == null + ? {} : { 1 = 1 } + ) + + content { + score = info_types.value.sensitivity_score + } + } + } + } + } + } + } + } + } + } + + dynamic "info_type_transformations" { + for_each = ( + deidentify_config.value.info_type_transformations == null + ? [] : [deidentify_config.value.info_type_transformations] + ) + + content { + dynamic "transformations" { + for_each = info_type_transformations.value.transformations + + content { + dynamic "info_types" { + for_each = ( + transformations.value.info_types == null + ? [] : transformations.value.info_types + ) + + content { + name = info_types.value.name + version = info_types.value.version + + dynamic "sensitivity_score" { + for_each = ( + info_types.value.sensitivity_score == null + ? [] : [1] + ) + + content { + score = info_types.value.sensitivity_score + } + } + } + } + + dynamic "primitive_transformation" { + for_each = [transformations.value.primitive_transformation] + + content { + dynamic "replace_config" { + for_each = ( + primitive_transformation.value.replace_config == null + ? [] : [primitive_transformation.value.replace_config] + ) + + content { + dynamic "new_value" { + for_each = [replace_config.value.new_value] + + content { + integer_value = new_value.value.integer_value + float_value = new_value.value.float_value + string_value = new_value.value.string_value + boolean_value = new_value.value.boolean_value + timestamp_value = new_value.value.timestamp_value + day_of_week_value = new_value.value.day_of_week_value + + dynamic "time_value" { + for_each = ( + new_value.value.time_value == null + ? [] : [new_value.value.time_value] + ) + + content { + hours = time_value.value.hours + minutes = time_value.value.minutes + seconds = time_value.value.seconds + nanos = time_value.value.nanos + } + } + + dynamic "date_value" { + for_each = ( + new_value.value.date_value == null + ? [] : [new_value.value.date_value] + ) + + content { + day = date_value.value.day + month = date_value.value.month + year = date_value.value.year + } + } + } + } + } + } + + dynamic "character_mask_config" { + for_each = ( + primitive_transformation.value.character_mask_config == null + ? [] : [primitive_transformation.value.character_mask_config] + ) + + content { + masking_character = character_mask_config.value.masking_character + number_to_mask = character_mask_config.value.number_to_mask + reverse_order = character_mask_config.value.reverse_order + + dynamic "characters_to_ignore" { + for_each = ( + character_mask_config.value.characters_to_ignore == null + ? [] : [character_mask_config.value.characters_to_ignore] + ) + + content { + characters_to_skip = characters_to_ignore.value.characters_to_skip + common_characters_to_ignore = characters_to_ignore.value.common_characters_to_ignore + } + } + } + } + + dynamic "crypto_deterministic_config" { + for_each = ( + primitive_transformation.value.crypto_deterministic_config == null + ? [] : [primitive_transformation.value.crypto_deterministic_config] + ) + + content { + dynamic "crypto_key" { + for_each = ( + crypto_deterministic_config.value.crypto_key == null + ? [] : [crypto_deterministic_config.value.crypto_key] + ) + + content { + dynamic "transient" { + for_each = ( + crypto_key.value.transient == null + ? [] : [crypto_key.value.transient] + ) + + content { + name = transient.value.name + } + } + + dynamic "unwrapped" { + for_each = ( + crypto_key.value.unwrapped == null + ? [] : [crypto_key.value.unwrapped] + ) + + content { + key = unwrapped.value.key + } + } + + dynamic "kms_wrapped" { + for_each = ( + crypto_key.value.kms_wrapped == null + ? [] : [crypto_key.value.kms_wrapped] + ) + + content { + wrapped_key = kms_wrapped.value.wrapped_key + crypto_key_name = kms_wrapped.value.crypto_key_name + } + } + } + } + + dynamic "surrogate_info_type" { + for_each = ( + crypto_deterministic_config.value.surrogate_info_type == null + ? [] : [crypto_deterministic_config.value.surrogate_info_type] + ) + + content { + name = surrogate_info_type.value.name + version = surrogate_info_type.value.version + + dynamic "sensitivity_score" { + for_each = ( + surrogate_info_type.value.sensitivity_score == null + ? {} : { 1 = 1 } + ) + + content { + score = surrogate_info_type.value.sensitivity_score + } + } + } + } + + dynamic "context" { + for_each = ( + crypto_deterministic_config.value.context == null + ? [] : [crypto_deterministic_config.value.context] + ) + + content { + name = context.value.name + } + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/modules/ai-applications/engines.tf b/modules/ai-applications/engines.tf index 02b69cc74..002d247f1 100644 --- a/modules/ai-applications/engines.tf +++ b/modules/ai-applications/engines.tf @@ -1,5 +1,5 @@ /** - * Copyright 2025 Google LLC + * Copyright 2026 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,84 +15,77 @@ */ resource "google_discovery_engine_chat_engine" "default" { - for_each = ({ - for k, v in var.engines_configs - : k => v if v.chat_engine_config != null - }) - engine_id = "${var.name}-${each.key}" - display_name = "${var.name}-${each.key}" - collection_id = each.value.collection_id + count = var.engines_configs.chat_engine_config == null ? 0 : 1 + engine_id = var.name + display_name = var.name + collection_id = var.engines_configs.collection_id project = var.project_id data_store_ids = [ - for ds in each.value.data_store_ids + for ds in var.engines_configs.data_store_ids : coalesce( try(google_discovery_engine_data_store.default[ds].data_store_id, null), ds ) ] industry_vertical = coalesce( - try(each.value.industry_vertical, null), - try(google_discovery_engine_data_store.default[each.value.data_store_ids[0]].industry_vertical, null) + try(var.engines_configs.industry_vertical, null), + try(google_discovery_engine_data_store.default[var.engines_configs.data_store_ids[0]].industry_vertical, null), + "GENERIC" ) location = coalesce( - try(each.value.location, null), - try(google_discovery_engine_data_store.default[each.value.data_store_ids[0]].location, null), + try(var.engines_configs.location, null), + try(google_discovery_engine_data_store.default[var.engines_configs.data_store_ids[0]].location, null), var.location ) chat_engine_config { - allow_cross_region = each.value.chat_engine_config.allow_cross_region - dialogflow_agent_to_link = each.value.chat_engine_config.dialogflow_agent_to_link - - agent_creation_config { - business = each.value.chat_engine_config.business - default_language_code = each.value.chat_engine_config.default_language_code - time_zone = each.value.chat_engine_config.time_zone - location = coalesce( - try(each.value.location, null), - try(google_discovery_engine_data_store.default[each.value.data_store_ids[0]].location, null), - var.location - ) - } + allow_cross_region = var.engines_configs.chat_engine_config.allow_cross_region + dialogflow_agent_to_link = try( + coalesce( + var.engines_configs.chat_engine_config.agent_config.id, + google_dialogflow_cx_agent.default[0].id + ), + null + ) } dynamic "common_config" { - for_each = each.value.chat_engine_config.company_name == null ? [] : [""] + for_each = ( + var.engines_configs.chat_engine_config.company_name == null + ? [] : [""] + ) content { - company_name = each.value.chat_engine_config.company_name + company_name = var.engines_configs.chat_engine_config.company_name } } } resource "google_discovery_engine_search_engine" "default" { - for_each = ({ - for k, v in var.engines_configs - : k => v if v.search_engine_config != null - }) - engine_id = "${var.name}-${each.key}" - display_name = "${var.name}-${each.key}" - collection_id = each.value.collection_id + count = var.engines_configs.search_engine_config == null ? 0 : 1 + engine_id = var.name + display_name = var.name + collection_id = var.engines_configs.collection_id project = var.project_id data_store_ids = [ - for ds in each.value.data_store_ids + for ds in var.engines_configs.data_store_ids : coalesce( try(google_discovery_engine_data_store.default[ds].data_store_id, null), ds ) ] industry_vertical = coalesce( - try(each.value.industry_vertical, null), - try(google_discovery_engine_data_store.default[each.value.data_store_ids[0]].industry_vertical, null) + try(var.engines_configs.industry_vertical, null), + try(google_discovery_engine_data_store.default[var.engines_configs.data_store_ids[0]].industry_vertical, null), + "GENERIC" ) location = coalesce( - try(each.value.location, null), - try(google_discovery_engine_data_store.default[each.value.data_store_ids[0]].location, null), + try(var.engines_configs.location, null), + try(google_discovery_engine_data_store.default[var.engines_configs.data_store_ids[0]].location, null), var.location ) - search_engine_config { - search_add_ons = each.value.search_engine_config.search_add_ons - search_tier = each.value.search_engine_config.search_tier + search_add_ons = var.engines_configs.search_engine_config.search_add_ons + search_tier = var.engines_configs.search_engine_config.search_tier } } diff --git a/modules/ai-applications/variables.tf b/modules/ai-applications/variables.tf index 37ce13efa..fc422fe50 100644 --- a/modules/ai-applications/variables.tf +++ b/modules/ai-applications/variables.tf @@ -1,5 +1,5 @@ /** - * Copyright 2025 Google LLC + * Copyright 2026 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,6 +14,294 @@ * limitations under the License. */ +variable "chat_agent_security_configs" { + description = "The DLP security configurations for (Dialogflow CX) chat agents." + type = object({ + audio_export_settings = optional(object({ + # AUDIO_FORMAT_UNSPECIFIED, MULAW, MP3, OGG + audio_format = string + gcs_bucket_config = object({ + # specify an id to use an existing GCS bucket + id = optional(string) + prefix = string + location = optional(string) + name = optional(string) + versioning = optional(bool, true) + }) + audio_export_pattern = string + enable_audio_redaction = optional(bool, false) + })) + dlp_deidentify_template = optional(object({ + description = optional(string, "Terraform managed.") + display_name = optional(string) + info_type_transformations = optional(object({ + transformations = list(object({ + info_types = optional(list(object({ + name = string + version = optional(string) + sensitivity_score = optional(string) + }))) + primitive_transformation = any + })) + })) + parent = optional(string) + record_transformations = optional(object({ + field_transformations = optional(list(object({ + fields = list(object({ name = string })) + condition = optional(object({ + expressions = optional(object({ + logical_operator = optional(string) + conditions = list(object({ + field = object({ name = string }) + operator = string + value = optional(object({ + integer_value = optional(number) + float_value = optional(number) + string_value = optional(string) + boolean_value = optional(bool) + timestamp_value = optional(string) + })) + })) + })) + })) + primitive_transformation = optional(object({ + replace_config = optional(object({ + new_value = object({ + integer_value = optional(number) + float_value = optional(number) + string_value = optional(string) + boolean_value = optional(bool) + timestamp_value = optional(string) + time_value = optional(object({ + hours = optional(number) + minutes = optional(number) + seconds = optional(number) + nanos = optional(number) + })) + date_value = optional(object({ + year = optional(number) + month = optional(number) + day = optional(number) + })) + day_of_week_value = optional(string) + }) + })) + character_mask_config = optional(object({ + masking_character = optional(string) + number_to_mask = optional(number) + reverse_order = optional(bool) + characters_to_ignore = optional(object({ + characters_to_skip = optional(string) + common_characters_to_ignore = optional(string) + })) + })) + crypto_replace_ffx_fpe_config = optional(object({ + crypto_key = optional(object({ + transient = optional(object({ name = string })) + unwrapped = optional(object({ key = string })) + kms_wrapped = optional(object({ wrapped_key = string, crypto_key_name = string })) + })) + context = optional(object({ name = optional(string) })) + surrogate_info_type = optional(object({ + name = optional(string) + version = optional(string) + sensitivity_score = optional(string) + })) + common_alphabet = optional(string) + custom_alphabet = optional(string) + radix = optional(number) + })) + # I'll use 'any' for the rest of primitive transformations here to keep it within reason, + # as they are less common in field transformations compared to masking/redaction/replacement. + # Actually, let's just include them as 'any' to save space but allow them. + crypto_deterministic_config = optional(any) + replace_dictionary_config = optional(any) + date_shift_config = optional(any) + fixed_size_bucketing_config = optional(any) + bucketing_config = optional(any) + time_part_config = optional(any) + redact_config = optional(bool) + crypto_hash_config = optional(any) + })) + info_type_transformations = optional(object({ + transformations = list(object({ + info_types = optional(list(object({ + name = string + version = optional(string) + sensitivity_score = optional(string) + }))) + primitive_transformation = optional(any) + })) + })) + }))) + record_suppressions = optional(list(object({ + condition = optional(object({ + expressions = optional(object({ + logical_operator = optional(string) + conditions = list(object({ + field = object({ name = string }) + operator = string + value = optional(object({ + integer_value = optional(number) + float_value = optional(number) + string_value = optional(string) + boolean_value = optional(bool) + timestamp_value = optional(string) + })) + })) + })) + })) + }))) + })) + template_id = optional(string) + })) + dlp_inspect_template = optional(object({ + # ["CONTENT_TEXT", "CONTENT_IMAGE"] + content_options = optional(list(string), []) + custom_info_types = optional(map(object({ + dictionary = optional(object({ + cloud_storage_path = optional(string) + words_list = optional(list(string)) + })) + # null or EXCLUSION_TYPE_EXCLUDE + exclusion_type = optional(string) + # SENSITIVITY_LOW, SENSITIVITY_MODERATE, SENSITIVITY_HIGH + # VERY_UNLIKELY, UNLIKELY, POSSIBLE, LIKELY, VERY_LIKELY + likelihood = optional(string, "VERY_LIKELY") + regex = optional(object({ + # https://github.com/google/re2/wiki/Syntax + pattern = string + group_indexes = optional(list(number)) + })) + sensitivity_score = optional(string) + stored_type_name = optional(string) + surrogate_type = optional(string) + version = optional(string) + }))) + description = optional(string, "Terraform managed.") + exclude_info_types = optional(bool, false) + include_quote = optional(bool, false) + # name is the key + # https://cloud.google.com/dlp/docs/infotypes-reference + info_types = optional(map(object({ + # SENSITIVITY_LOW, SENSITIVITY_MODERATE, SENSITIVITY_HIGH + sensitivity_score = optional(string, "SENSITIVITY_MODERATE") + version = optional(string) + })), {}) + limits = optional(object({ + max_findings_per_item = optional(number, 2000) + max_findings_per_request = optional(number, 2000) + # key is the name of the info type + # https://cloud.google.com/dlp/docs/infotypes-reference + max_findings_per_info_type = optional(map(object({ + max_findings = optional(number, 2000) + # SENSITIVITY_LOW, SENSITIVITY_MODERATE, SENSITIVITY_HIGH + sensitivity_score = optional(string, "SENSITIVITY_MODERATE") + version = optional(string) + }))) + })) + # VERY_UNLIKELY, UNLIKELY, POSSIBLE, LIKELY, VERY_LIKELY + min_likelihood = optional(string, "POSSIBLE") + name = optional(string) + parent = optional(string) + rule_sets = optional(map(object({ + # name is the key + # https://cloud.google.com/dlp/docs/infotypes-reference + info_types = map(object({ + version = optional(string) + # SENSITIVITY_LOW, SENSITIVITY_MODERATE, SENSITIVITY_HIGH + sensitivity_score = optional(string, "SENSITIVITY_MODERATE") + })) + rules = object({ + exclusion_rule = optional(object({ + # MATCHING_TYPE_FULL_MATCH, MATCHING_TYPE_PARTIAL_MATCH, MATCHING_TYPE_INVERSE_MATCH + # https://cloud.google.com/dlp/docs/reference/rest/v2/InspectConfig#MatchingType + matching_type = string + dictionary = optional(object({ + cloud_storage_path = optional(string) + words_list = optional(list(string)) + })) + regex = optional(object({ + # https://github.com/google/re2/wiki/Syntax + pattern = string + group_indexes = optional(list(number)) + })) + })) + hotword_rule = optional(object({ + hotword_regex = object({ + pattern = string + group_indexes = optional(list(number)) + }) + proximity = object({ + # Either window_before or window_after must be specified + window_after = optional(number) + window_before = optional(number) + }) + likelihood_adjustment = optional(object({ + fixed_likelihood = optional(string) + relative_likelihood = optional(number) + })) + })) + }) + })), {}) + # [a-zA-Z\d-_]+. The maximum length is 100 characters. + # Auto-generated if null. + template_id = optional(string) + })) + enable_insights_export = optional(bool, false) + location = optional(string) + purge_data_types = optional(list(string)) + redaction_scope = optional(string) + redaction_strategy = optional(string) + retention_window_days = optional(number) + }) + nullable = false + default = {} + validation { + condition = try(var.chat_agent_security_configs.dlp_inspect_template.min_likelihood == null ? true : contains( + ["VERY_UNLIKELY", "UNLIKELY", "POSSIBLE", "LIKELY", "VERY_LIKELY"], + var.chat_agent_security_configs.dlp_inspect_template.min_likelihood + ), true) + error_message = "inspect_template.min_likelihood must be one of [VERY_UNLIKELY, UNLIKELY, POSSIBLE, LIKELY, VERY_LIKELY]." + } + validation { + condition = alltrue([ + for k, v in try(var.chat_agent_security_configs.dlp_inspect_template.custom_info_types, {}) : v.likelihood == null ? true : contains( + ["VERY_UNLIKELY", "UNLIKELY", "POSSIBLE", "LIKELY", "VERY_LIKELY"], + v.likelihood + ) + ]) + error_message = "inspect_template.custom_info_types.*.likelihood must be one of [VERY_UNLIKELY, UNLIKELY, POSSIBLE, LIKELY, VERY_LIKELY]." + } + validation { + condition = alltrue([ + for k, v in try(var.chat_agent_security_configs.dlp_inspect_template.rule_sets, {}) : try(v.rules.exclusion_rule.matching_type == null ? true : contains( + ["MATCHING_TYPE_FULL_MATCH", "MATCHING_TYPE_PARTIAL_MATCH", "MATCHING_TYPE_INVERSE_MATCH"], + v.rules.exclusion_rule.matching_type + ), true) + ]) + error_message = "inspect_template.rule_sets.*.rules.exclusion_rule.matching_type must be one of [MATCHING_TYPE_FULL_MATCH, MATCHING_TYPE_PARTIAL_MATCH, MATCHING_TYPE_INVERSE_MATCH]." + } + validation { + condition = alltrue([ + for k, v in try(var.chat_agent_security_configs.dlp_inspect_template.custom_info_types, {}) : v.exclusion_type == null ? true : contains( + ["EXCLUSION_TYPE_EXCLUDE"], + v.exclusion_type + ) + ]) + error_message = "inspect_template.custom_info_types.*.exclusion_type must be EXCLUSION_TYPE_EXCLUDE." + } + validation { + condition = alltrue([ + for k, v in try(var.chat_agent_security_configs.dlp_inspect_template.info_types, {}) : v.sensitivity_score == null ? true : contains( + ["SENSITIVITY_LOW", "SENSITIVITY_MODERATE", "SENSITIVITY_HIGH"], + v.sensitivity_score + ) + ]) + error_message = "inspect_template.info_types.*.sensitivity_score must be one of [SENSITIVITY_LOW, SENSITIVITY_MODERATE, SENSITIVITY_HIGH]." + } +} + variable "data_stores_configs" { description = "The ai-applications datastore configurations." type = map(object({ @@ -64,76 +352,109 @@ variable "data_stores_configs" { nullable = false default = {} validation { - condition = try(contains( - ["CONTENT_REQUIRED", "NO_CONTENT", "PUBLIC_WEBSITE"], - var.data_stores_configs.content_config - ), true) - error_message = "data_store_configs.content_config must be one or more of [CONTENT_REQUIRED, NO_CONTENT, PUBLIC_WEBSITE]." - } - validation { - condition = try(contains( - ["GENERIC", "HEALTHCARE_FHIR", "MEDIA"], - var.data_stores_configs.industry_vertical - ), true) - error_message = "data_store_configs.industry_vertical must be one or more of [GENERIC, HEALTHCARE_FHIR, MEDIA]." + condition = alltrue([ + for k, v in var.data_stores_configs : contains( + ["CONTENT_REQUIRED", "NO_CONTENT", "PUBLIC_WEBSITE"], + v.content_config + ) + ]) + error_message = "data_store_configs.content_config must be one of [CONTENT_REQUIRED, NO_CONTENT, PUBLIC_WEBSITE]." } validation { condition = alltrue([ - for st in try(var.data_stores_configs.solution_types, []) - : contains([ - "SOLUTION_TYPE_CHAT", - "SOLUTION_TYPE_GENERATIVE_CHAT", - "SOLUTION_TYPE_RECOMMENDATION", - "SOLUTION_TYPE_SEARCH" - ], st) + for k, v in var.data_stores_configs : contains( + ["GENERIC", "HEALTHCARE_FHIR", "MEDIA"], + v.industry_vertical + ) ]) + error_message = "data_store_configs.industry_vertical must be one of [GENERIC, HEALTHCARE_FHIR, MEDIA]." + } + validation { + condition = alltrue(flatten([ + for k, v in var.data_stores_configs : [ + for st in try(v.solution_types, []) : contains([ + "SOLUTION_TYPE_CHAT", + "SOLUTION_TYPE_GENERATIVE_CHAT", + "SOLUTION_TYPE_RECOMMENDATION", + "SOLUTION_TYPE_SEARCH" + ], st) + ] + ])) error_message = "data_store_configs.solution_types must be one or more of [SOLUTION_TYPE_CHAT, SOLUTION_TYPE_GENERATIVE_CHAT, SOLUTION_TYPE_RECOMMENDATION, SOLUTION_TYPE_SEARCH]." } validation { - condition = alltrue([ - for k, _ in try(var.data_stores_configs.document_processing_config.parsing_config_overrides, {}) - : contains([ - "docx", - "html", - "pdf" - ], k) - ]) + condition = alltrue(flatten([ + for k, v in var.data_stores_configs : [ + for po_k, po_v in try(v.document_processing_config.parsing_config_overrides, {}) : contains([ + "docx", + "html", + "pdf" + ], po_k) + ] + ])) error_message = "keys in var.data_stores_configs.document_processing_config.parsing_config_overrides must be one of [docx, html, pdf]." } validation { - condition = try(contains( - ["EXCLUDE", "INCLUDE"], - var.data_stores_configs.target_site_config - ), true) - error_message = "data_store_configs.target_site_config must be one or more of [EXCLUDE, INCLUDE]." + condition = alltrue(flatten([ + for k, v in var.data_stores_configs : [ + for ts_k, ts_v in try(v.sites_search_config.target_sites, {}) : contains([ + "EXCLUDE", + "INCLUDE" + ], ts_v.type) + ] + ])) + error_message = "data_store_configs.sites_search_config.target_sites.*.type must be one of [EXCLUDE, INCLUDE]." } } variable "engines_configs" { - description = "The ai-applications engines configurations." - type = map(object({ - data_store_ids = list(string) - collection_id = optional(string, "default_collection") + description = "The AI applications engines configurations." + type = object({ chat_engine_config = optional(object({ - allow_cross_region = optional(bool, null) - business = optional(string) - company_name = optional(string) - default_language_code = optional(string) - dialogflow_agent_to_link = optional(string) - time_zone = optional(string) + agent_config = optional(object({ + avatar_uri = optional(string) + default_language_code = optional(string) + description = optional(string, "Terraform managed.") + # Id of an existing agent. The agent will be created otherwise. + id = optional(string) + # This overrides the engine location, + # the datastores location and var.location + location = optional(string) + security_settings_config = optional(object({ + create = optional(bool, false) + id = optional(string) + })) + supported_language_codes = optional(list(string)) + time_zone = optional(string) + }), {}) + allow_cross_region = optional(bool, true) + business = optional(string) + company_name = optional(string) })) + collection_id = optional(string, "default_collection") + data_store_ids = optional(list(string), []) # If industry_vertical and location are not given, # they are derived from the first datastore attached # to the engines industry_vertical = optional(string) - location = optional(string) + # This can override var.location and the datastores location + location = optional(string) search_engine_config = optional(object({ search_add_ons = optional(list(string), []) search_tier = optional(string) })) - })) + }) nullable = false default = {} + validation { + condition = ( + var.engines_configs.chat_engine_config == null + && var.engines_configs.search_engine_config == null + ? true + : length(var.engines_configs.data_store_ids) > 0 ? true : false + ) + error_message = "You must specify at least one data store id for each engine." + } validation { condition = alltrue([ for ao in try(var.engines_configs.search_engine_config.search_add_ons, []) @@ -142,10 +463,13 @@ variable "engines_configs" { error_message = "Elements in engines_configs.search_engine_config.search_add_ons must be one or more of [SEARCH_ADD_ON_LLM]." } validation { - condition = try(contains( - ["SEARCH_TIER_ENTERPRISE", "SEARCH_TIER_STANDARD"], - var.engines_configs.search_engine_config.search_tier - ), true) + condition = alltrue([ + try(var.engines_configs.search_engine_config.search_tier, null) == null + ? true : contains( + ["SEARCH_TIER_ENTERPRISE", "SEARCH_TIER_STANDARD"], + var.engines_configs.search_engine_config.search_tier + ), true + ]) error_message = "engines_configs.search_engine_config.search_tier must be one of [SEARCH_TIER_ENTERPRISE, SEARCH_TIER_STANDARD]." } }