从 Dialogflow ES 迁移到 Conversational Agents (Dialogflow CX)

与 Dialogflow ES 代理相比,Conversational Agents (Dialogflow CX) 代理可为您提供更强大的对话控制功能和工具。如果您的 Dialogflow ES 客服处理复杂对话,您应考虑迁移到对话式客服 (Dialogflow CX)。

本指南介绍了如何将代理从 Dialogflow ES 迁移到 Conversational Agents (Dialogflow CX)。这两种代理类型存在许多根本差异,因此无法直接执行此迁移。

如果您在迁移过程中使用了本指南,请点击上方的发送反馈按钮,提供正面或负面反馈。我们会根据这些反馈不断改进本指南。

概括来讲,建议采用自动化/手动混合流程。您将使用一款工具来读取部分 Dialogflow ES 代理数据,将这些数据写入 Conversational Agents (Dialogflow CX) 代理,并捕获 TODO 列表。然后,您可以使用最佳实践、TODO 列表和工具迁移的数据重新创建完整的 Conversational Agents (Dialogflow CX) 代理。

了解对话式 AI 客服 (Dialogflow CX)

在尝试进行此迁移之前,您应充分了解对话式 AI 助理 (Dialogflow CX) 流程的运作方式。您可以从以下位置开始:

您还应阅读其他概念文档,了解新代理中可能需要的功能。重点关注以下几点:

了解 Dialogflow ES/Conversational Agents (Dialogflow CX) 之间的差异

本部分列出了 Dialogflow Dialogflow ES 和 Conversational Agents (Dialogflow CX) 之间的最重要的区别。稍后执行手动迁移步骤时,您应参阅本部分以获取指导。

结构和对话路径控制

Dialogflow ES 提供了以下结构和对话路径控制功能:

  • intent 用作客服助理的构建块。在对话的任何时间点,系统都会匹配到一个 intent,从某种意义上讲,每个 intent 都是对话的一个节点。
  • 上下文用于控制对话。上下文用于控制在任何给定时间可以匹配哪些 intent。上下文会在一定数量的对话回合后过期,因此对于长对话,此类控制可能不准确。

Conversational Agents (Dialogflow CX) 提供了结构资源层次结构,并对对话路径提供了更精确的控制:

  • 页面是对话的图节点。对话式 AI 客服 (Dialogflow CX) 对话类似于状态机。在对话的任意时间点,只有一个页面处于活跃状态。根据最终用户输入或事件,对话可能会转换到其他页面。网页通常会在多轮对话中保持活跃状态。
  • 流程是一系列相关网页。每个流都应处理一个高级对话主题。
  • 状态处理程序用于控制转换和响应。状态处理程序有三种类型:
    • intent 路由:包含必须匹配的意图、可选响应和可选页面转换。
    • 条件路由:包含必须满足的条件、可选响应和可选页面转换。
    • 事件处理脚本:包含必须调用的事件名称、可选响应和可选页面转换。
  • 作用域用于控制是否可以调用状态处理程序。大多数处理脚本都与网页或整个流程相关联。如果关联的页面或流处于活跃状态,则处理程序在范围内,并且可以调用。范围内的 Conversational Agents (Dialogflow CX) 意图路由类似于输入上下文处于活动状态的 Dialogflow ES 意图。

在设计代理的流程和页面时,请务必了解代理设计指南的“流程”部分中的建议。

表单填充

Dialogflow ES 使用槽位填充功能从最终用户处收集必需参数:

  • 这些参数是标记为必需的 intent 参数。
  • 系统会继续匹配意图,直到收集到所有必需参数为止。
  • 您可以定义一个提示,要求最终用户提供值。

对话式 AI 助理 (Dialogflow CX) 使用表单填充功能从最终用户收集所需参数:

  • 这些参数与网页相关联,并会在网页处于活跃状态时收集。
  • 您可以使用页面条件路由来确定表单填写是否已完成。这些条件路由通常会转换到其他页面。
  • 您可以定义提示以及重新提示处理脚本,以妥善处理多次尝试收集值的情况。

转换

当最终用户输入与某个 intent 匹配时,Dialogflow ES 会自动从一个 intent 转换到下一个 intent。只有没有输入上下文或具有活跃输入上下文的意图才能进行这种匹配。

当范围内的状态处理程序满足其要求并提供转换目标时,Conversational Agents (Dialogflow CX) 会从一个页面转换到下一个页面。借助这些转场效果,您可以可靠地引导最终用户完成对话。您可以通过多种方式控制这些转场效果:

  • 意图匹配可以触发意图路由。
  • 满足某个条件可以触发条件路由。
  • 调用事件可以触发事件处理脚本。
  • 如果最终用户在多次尝试后仍无法提供值,重新提示处理程序可能会导致转换。
  • 您可以将符号转换目标用作转换目标。

客服人员回复

当系统匹配到某个 intent 时,Dialogflow ES 代理响应会发送给最终用户:

  • 客服人员可以从可能的回答列表中选择一条消息作为回复。
  • 响应可以是平台专用,可以使用丰富的响应格式
  • 响应可以由 webhook 触发。

调用执行时,系统会将 Conversational Agents (Dialogflow CX) 代理响应发送给最终用户。与 Dialogflow ES 执行方式(始终涉及网络钩子)不同,对话式代理 (Dialogflow CX) 执行方式可能或不一定涉及调用网络钩子,具体取决于执行方式资源是否已配置网络钩子。基于 webhook 响应的静态响应和动态响应均由 fulfillment 控制。您可以通过多种方式创建客服人员响应:

  • 可向任何类型的状态处理程序提供执行方式。
  • 在对话回合期间,可以通过响应队列串联多条响应。在某些情况下,此功能可以简化代理设计。
  • Conversational Agents (Dialogflow CX) 不支持平台专用内置响应。不过,它提供了多种响应类型,包括可用于平台专用响应的自定义载荷。

参数

Dialogflow ES 参数具有以下特征:

  • 仅在 intent 中定义。
  • 由最终用户输入、事件、webhook 和 API 调用设置。
  • 在响应、参数提示、网络钩子代码和参数值中引用:
    • 基本参考格式为 $parameter-name
    • 参考文档支持 .original.partial.recent 后缀语法。
    • 引用可以指定活跃上下文:#context-name.parameter-name
    • 引用可以指定事件参数:#event-name.parameter-name

Conversational Agents (Dialogflow CX) 参数具有以下特点:

  • 在 intent 和页面表单中定义。
  • 意图和表单参数会传播到会话参数,在会话期间可供引用。
  • 由最终用户输入、网络钩子、执行参数预设和 API 调用设置。
  • 在响应、参数提示、重新提示处理脚本、参数预设和 Webhook 代码中引用:
    • 引用格式为:会话参数为 $session.params.parameter-id,意图参数为 $intent.params.parameter-id
    • intent 参数引用支持 .original.resolved 后缀语法。会话参数不支持此语法。

系统实体

Dialogflow ES 支持许多系统实体

Conversational Agents (Dialogflow CX) 支持许多相同的系统实体,但也存在一些差异。迁移时,请验证您在 Dialogflow ES 中使用的系统实体是否也受对应语言的对话式客服 (Dialogflow CX) 支持。如果没有,您应为这些实体创建自定义实体。

活动

Dialogflow ES 事件具有以下特征:

  • 可通过 API 调用或 webhook 调用,以匹配 intent。
  • 可以设置参数。
  • 集成平台会调用少量事件。

Conversational Agents (Dialogflow CX) 事件具有以下特点:

  • 可通过 API 调用或 webhook 调用,以调用事件处理脚本。
  • 无法设置参数。
  • 许多内置事件可用于处理缺少最终用户输入、无法识别的最终用户输入、由网络钩子失效的参数以及网络钩子错误。
  • 调用可以通过与其他状态处理程序相同的作用域规则进行控制。

内置意图

Dialogflow ES 支持以下内置 intent:

以下介绍了 Conversational Agents (Dialogflow CX) 对内置 intent 的支持:

  • 支持欢迎 intent
  • 不提供回退 intent。请改为在事件处理脚本中使用不匹配事件。
  • 对于反例,请使用默认负意图
  • 不提供预定义的后续 intent。您必须根据代理的要求创建这些 intent。例如,您可能需要创建 intent 来处理对客服人员问题的否定回答(“不”“不用了”“不,我没有”等)。Conversational Agents (Dialogflow CX) 意图可在您的代理中重复使用,因此您只需定义一次即可。在不同作用域中,为这些常见 intent 使用不同的 intent 路由,可让您更好地控制对话。

Webhook

Dialogflow ES Webhook 具有以下特征:

  • 您可以为代理配置一个 webhook 服务。
  • 每个 intent 都可以标记为使用该 webhook。
  • 没有内置的 webhook 错误处理支持。
  • 聊天操作或聊天名称由 webhook 用于确定其是在代理中的哪个位置被调用的。
  • 该控制台提供了内嵌编辑器

Conversational Agents (Dialogflow CX) webhook 具有以下特点:

  • 您可以为代理配置多个 webhook 服务。
  • 每个 fulfillment 都可以选择性地指定 webhook 调用。
  • 内置支持 Webhook 错误处理
  • Conversational Agents (Dialogflow CX) 执行 webhook 包含一个标记。此代码类似于 Dialogflow ES 操作,但仅在调用 webhook 时使用。Webhook 服务可以使用这些标记来确定它是在代理中的哪个位置被调用的。
  • 该控制台没有内置的 webhook 代码编辑器。通常使用 Cloud Functions 即可,但还有许多其他选项。

迁移到 Conversational Agents (Dialogflow CX) 时,您需要更改网络钩子代码,因为请求和响应属性不同。

集成

Dialogflow ES 集成Conversational Agents (Dialogflow CX) 集成支持不同的平台。对于这两种代理类型都支持的平台,配置可能有所不同。

如果对话代理 (Dialogflow CX) 不支持您使用的 Dialogflow ES 集成,您可能需要切换平台或自行实现集成。

更多仅限 Conversational Agents (Dialogflow CX) 的功能

还有许多其他功能仅由 Conversational Agents (Dialogflow CX) 提供。您应考虑在迁移过程中使用这些功能。 例如:

最佳做法

在迁移之前,请先熟悉对话式客服 (Dialogflow CX) 客服设计最佳实践。其中许多 Conversational Agents (Dialogflow CX) 最佳实践与 Dialogflow ES 最佳实践类似,但有些是 Conversational Agents (Dialogflow CX) 独有的。

关于迁移工具

迁移工具会将大部分 Dialogflow ES 数据复制到您的 Conversational Agents (Dialogflow CX) 代理,并将必须手动迁移的项目列表写入 TODO 文件。该工具仅会复制自定义实体类型和意图训练短语。您应考虑根据自己的具体需求自定义此工具。

迁移工具代码

下面是该工具的代码。 您应查看此工具的代码,以了解其用途。您可能需要更改此代码,以便在代理中处理特定情况。在以下步骤中,您将执行此工具。

// Package main implements the ES to CX migration tool.
package main

import (
	"context"
	"encoding/csv"
	"flag"
	"fmt"
	"os"
	"strings"
	"time"

	v2 "cloud.google.com/go/dialogflow/apiv2"
	proto2 "cloud.google.com/go/dialogflow/apiv2/dialogflowpb"
	v3 "cloud.google.com/go/dialogflow/cx/apiv3"
	proto3 "cloud.google.com/go/dialogflow/cx/apiv3/cxpb"
	"google.golang.org/api/iterator"
	"google.golang.org/api/option"
)

// Commandline flags
var v2Project *string = flag.String("es-project-id", "", "ES project")
var v3Project *string = flag.String("cx-project-id", "", "CX project")
var v2Region *string = flag.String("es-region-id", "", "ES region")
var v3Region *string = flag.String("cx-region-id", "", "CX region")
var v3Agent *string = flag.String("cx-agent-id", "", "CX region")
var outFile *string = flag.String("out-file", "", "Output file for CSV TODO items")
var dryRun *bool = flag.Bool("dry-run", false, "Set true to skip CX agent writes")

// Map from entity type display name to fully qualified name.
var entityTypeShortToLong = map[string]string{}

// Map from ES system entity to CX system entity
var convertSystemEntity = map[string]string{
	"sys.address":         "sys.address",
	"sys.any":             "sys.any",
	"sys.cardinal":        "sys.cardinal",
	"sys.color":           "sys.color",
	"sys.currency-name":   "sys.currency-name",
	"sys.date":            "sys.date",
	"sys.date-period":     "sys.date-period",
	"sys.date-time":       "sys.date-time",
	"sys.duration":        "sys.duration",
	"sys.email":           "sys.email",
	"sys.flight-number":   "sys.flight-number",
	"sys.geo-city-gb":     "sys.geo-city",
	"sys.geo-city-us":     "sys.geo-city",
	"sys.geo-city":        "sys.geo-city",
	"sys.geo-country":     "sys.geo-country",
	"sys.geo-state":       "sys.geo-state",
	"sys.geo-state-us":    "sys.geo-state",
	"sys.geo-state-gb":    "sys.geo-state",
	"sys.given-name":      "sys.given-name",
	"sys.language":        "sys.language",
	"sys.last-name":       "sys.last-name",
	"sys.street-address":  "sys.location",
	"sys.location":        "sys.location",
	"sys.number":          "sys.number",
	"sys.number-integer":  "sys.number-integer",
	"sys.number-sequence": "sys.number-sequence",
	"sys.ordinal":         "sys.ordinal",
	"sys.percentage":      "sys.percentage",
	"sys.person":          "sys.person",
	"sys.phone-number":    "sys.phone-number",
	"sys.temperature":     "sys.temperature",
	"sys.time":            "sys.time",
	"sys.time-period":     "sys.time-period",
	"sys.unit-currency":   "sys.unit-currency",
	"sys.url":             "sys.url",
	"sys.zip-code":        "sys.zip-code",
}

// Issues found for the CSV output
var issues = [][]string{
	{"Field", "Issue"},
}

// logIssue logs an issue for the CSV output
func logIssue(field string, issue string) {
	issues = append(issues, []string{field, issue})
}

// convertEntityType converts an ES entity type to CX
func convertEntityType(et2 *proto2.EntityType) *proto3.EntityType {
	var kind3 proto3.EntityType_Kind
	switch kind2 := et2.Kind; kind2 {
	case proto2.EntityType_KIND_MAP:
		kind3 = proto3.EntityType_KIND_MAP
	case proto2.EntityType_KIND_LIST:
		kind3 = proto3.EntityType_KIND_LIST
	case proto2.EntityType_KIND_REGEXP:
		kind3 = proto3.EntityType_KIND_REGEXP
	default:
		kind3 = proto3.EntityType_KIND_UNSPECIFIED
	}
	var expansion3 proto3.EntityType_AutoExpansionMode
	switch expansion2 := et2.AutoExpansionMode; expansion2 {
	case proto2.EntityType_AUTO_EXPANSION_MODE_DEFAULT:
		expansion3 = proto3.EntityType_AUTO_EXPANSION_MODE_DEFAULT
	default:
		expansion3 = proto3.EntityType_AUTO_EXPANSION_MODE_UNSPECIFIED
	}
	et3 := &proto3.EntityType{
		DisplayName:           et2.DisplayName,
		Kind:                  kind3,
		AutoExpansionMode:     expansion3,
		EnableFuzzyExtraction: et2.EnableFuzzyExtraction,
	}
	for _, e2 := range et2.Entities {
		et3.Entities = append(et3.Entities, &proto3.EntityType_Entity{
			Value:    e2.Value,
			Synonyms: e2.Synonyms,
		})
	}
	return et3
}

// convertParameterEntityType converts a entity type found in parameters
func convertParameterEntityType(intent string, parameter string, t2 string) string {
	if len(t2) == 0 {
		return ""
	}
	t2 = t2[1:] // remove @
	if strings.HasPrefix(t2, "sys.") {
		if val, ok := convertSystemEntity[t2]; ok {
			t2 = val
		} else {
			t2 = "sys.any"
			logIssue("Intent<"+intent+">.Parameter<"+parameter+">",
				"This intent parameter uses a system entity not supported by CX English agents. See the migration guide for advice. System entity: "+t2)
		}
		return fmt.Sprintf("projects/-/locations/-/agents/-/entityTypes/%s", t2)
	}
	return entityTypeShortToLong[t2]
}

// convertIntent converts an ES intent to CX
func convertIntent(intent2 *proto2.Intent) *proto3.Intent {
	if intent2.DisplayName == "Default Fallback Intent" ||
		intent2.DisplayName == "Default Welcome Intent" {
		return nil
	}

	intent3 := &proto3.Intent{
		DisplayName: intent2.DisplayName,
	}

	// WebhookState
	if intent2.WebhookState != proto2.Intent_WEBHOOK_STATE_UNSPECIFIED {
		logIssue("Intent<"+intent2.DisplayName+">.WebhookState",
			"This intent has webhook enabled. You must configure this in your CX agent.")
	}

	// IsFallback
	if intent2.IsFallback {
		logIssue("Intent<"+intent2.DisplayName+">.IsFallback",
			"This intent is a fallback intent. CX does not support this. Use no-match events instead.")
	}

	// MlDisabled
	if intent2.MlDisabled {
		logIssue("Intent<"+intent2.DisplayName+">.MlDisabled",
			"This intent has ML disabled. CX does not support this.")
	}

	// LiveAgentHandoff
	if intent2.LiveAgentHandoff {
		logIssue("Intent<"+intent2.DisplayName+">.LiveAgentHandoff",
			"This intent uses live agent handoff. You must configure this in a fulfillment.")
	}

	// EndInteraction
	if intent2.EndInteraction {
		logIssue("Intent<"+intent2.DisplayName+">.EndInteraction",
			"This intent uses end interaction. CX does not support this.")
	}

	// InputContextNames
	if len(intent2.InputContextNames) > 0 {
		logIssue("Intent<"+intent2.DisplayName+">.InputContextNames",
			"This intent uses context. See the migration guide for alternatives.")
	}

	// Events
	if len(intent2.Events) > 0 {
		logIssue("Intent<"+intent2.DisplayName+">.Events",
			"This intent uses events. Use event handlers instead.")
	}

	// TrainingPhrases
	var trainingPhrases3 []*proto3.Intent_TrainingPhrase
	for _, tp2 := range intent2.TrainingPhrases {
		if tp2.Type == proto2.Intent_TrainingPhrase_TEMPLATE {
			logIssue("Intent<"+intent2.DisplayName+">.TrainingPhrases",
				"This intent has a training phrase that uses a template (@...) training phrase type. CX does not support this.")
		}
		var parts3 []*proto3.Intent_TrainingPhrase_Part
		for _, part2 := range tp2.Parts {
			parts3 = append(parts3, &proto3.Intent_TrainingPhrase_Part{
				Text:        part2.Text,
				ParameterId: part2.Alias,
			})
		}
		trainingPhrases3 = append(trainingPhrases3, &proto3.Intent_TrainingPhrase{
			Parts:       parts3,
			RepeatCount: 1,
		})
	}
	intent3.TrainingPhrases = trainingPhrases3

	// Action
	if len(intent2.Action) > 0 {
		logIssue("Intent<"+intent2.DisplayName+">.Action",
			"This intent sets the action field. Use a fulfillment webhook tag instead.")
	}

	// OutputContexts
	if len(intent2.OutputContexts) > 0 {
		logIssue("Intent<"+intent2.DisplayName+">.OutputContexts",
			"This intent uses context. See the migration guide for alternatives.")
	}

	// ResetContexts
	if intent2.ResetContexts {
		logIssue("Intent<"+intent2.DisplayName+">.ResetContexts",
			"This intent uses context. See the migration guide for alternatives.")
	}

	// Parameters
	var parameters3 []*proto3.Intent_Parameter
	for _, p2 := range intent2.Parameters {
		if len(p2.Value) > 0 && p2.Value != "$"+p2.DisplayName {
			logIssue("Intent<"+intent2.DisplayName+">.Parameters<"+p2.DisplayName+">.Value",
				"This field is not set to $parameter-name. This feature is not supported by CX. See: https://cloud.google.com/dialogflow/es/docs/intents-actions-parameters#valfield.")
		}
		if len(p2.DefaultValue) > 0 {
			logIssue("Intent<"+intent2.DisplayName+">.Parameters<"+p2.DisplayName+">.DefaultValue",
				"This intent parameter is using a default value. CX intent parameters do not support default values, but CX page form parameters do. This parameter should probably become a form parameter.")
		}
		if p2.Mandatory {
			logIssue("Intent<"+intent2.DisplayName+">.Parameters<"+p2.DisplayName+">.Mandatory",
				"This intent parameter is marked as mandatory. CX intent parameters do not support mandatory parameters, but CX page form parameters do. This parameter should probably become a form parameter.")
		}
		for _, prompt := range p2.Prompts {
			logIssue("Intent<"+intent2.DisplayName+">.Parameters<"+p2.DisplayName+">.Prompts",
				"This intent parameter has a prompt. Use page form parameter prompts instead. Prompt: "+prompt)
		}
		if len(p2.EntityTypeDisplayName) == 0 {
			p2.EntityTypeDisplayName = "@sys.any"
			logIssue("Intent<"+intent2.DisplayName+">.Parameters<"+p2.DisplayName+">.EntityTypeDisplayName",
				"This intent parameter does not have an entity type. CX requires an entity type for all parameters..")
		}
		parameters3 = append(parameters3, &proto3.Intent_Parameter{
			Id:         p2.DisplayName,
			EntityType: convertParameterEntityType(intent2.DisplayName, p2.DisplayName, p2.EntityTypeDisplayName),
			IsList:     p2.IsList,
		})
		//fmt.Printf("Converted parameter: %+v\n", parameters3[len(parameters3)-1])
	}
	intent3.Parameters = parameters3

	// Messages
	for _, message := range intent2.Messages {
		m, ok := message.Message.(*proto2.Intent_Message_Text_)
		if ok {
			for _, t := range m.Text.Text {
				warnings := ""
				if strings.Contains(t, "#") {
					warnings += " This message may contain a context parameter reference, but CX does not support this."
				}
				if strings.Contains(t, ".original") {
					warnings += " This message may contain a parameter reference suffix of '.original', But CX only supports this for intent parameters (not session parameters)."
				}
				if strings.Contains(t, ".recent") {
					warnings += " This message may contain a parameter reference suffix of '.recent', but CX does not support this."
				}
				if strings.Contains(t, ".partial") {
					warnings += " This message may contain a parameter reference suffix of '.partial', but CX does not support this."
				}
				logIssue("Intent<"+intent2.DisplayName+">.Messages",
					"This intent has a response message. Use fulfillment instead."+warnings+" Message: "+t)
			}
		} else {
			logIssue("Intent<"+intent2.DisplayName+">.Messages",
				"This intent has a non-text response message. See the rich response message information in the migration guide.")
		}
		if message.Platform != proto2.Intent_Message_PLATFORM_UNSPECIFIED {
			logIssue("Intent<"+intent2.DisplayName+">.Platform",
				"This intent has a message with a non-default platform. See the migration guide for advice.")
		}
	}

	return intent3
}

// migrateEntities migrates ES entities to your CX agent
func migrateEntities(ctx context.Context) error {
	var err error

	// Create ES client
	var client2 *v2.EntityTypesClient
	options2 := []option.ClientOption{}
	if len(*v2Region) > 0 {
		options2 = append(options2,
			option.WithEndpoint(*v2Region+"-dialogflow.googleapis.com:443"))
	}
	client2, err = v2.NewEntityTypesClient(ctx, options2...)
	if err != nil {
		return err
	}
	defer client2.Close()
	var parent2 string
	if len(*v2Region) == 0 {
		parent2 = fmt.Sprintf("projects/%s/agent", *v2Project)
	} else {
		parent2 = fmt.Sprintf("projects/%s/locations/%s/agent", *v2Project, *v2Region)
	}

	// Create CX client
	var client3 *v3.EntityTypesClient
	options3 := []option.ClientOption{}
	if len(*v3Region) > 0 {
		options3 = append(options3,
			option.WithEndpoint(*v3Region+"-dialogflow.googleapis.com:443"))
	}
	client3, err = v3.NewEntityTypesClient(ctx, options3...)
	if err != nil {
		return err
	}
	defer client3.Close()
	parent3 := fmt.Sprintf("projects/%s/locations/%s/agents/%s", *v3Project, *v3Region, *v3Agent)

	// Read each V2 entity type, convert, and write to V3
	request2 := &proto2.ListEntityTypesRequest{
		Parent: parent2,
	}
	it2 := client2.ListEntityTypes(ctx, request2)
	for {
		var et2 *proto2.EntityType
		et2, err = it2.Next()
		if err == iterator.Done {
			break
		}
		if err != nil {
			return err
		}
		fmt.Printf("Entity Type: %s\n", et2.DisplayName)

		if *dryRun {
			convertEntityType(et2)
			continue
		}

		request3 := &proto3.CreateEntityTypeRequest{
			Parent:     parent3,
			EntityType: convertEntityType(et2),
		}
		et3, err := client3.CreateEntityType(ctx, request3)
		entityTypeShortToLong[et3.DisplayName] = et3.Name
		if err != nil {
			return err
		}

		// ES and CX each have a quota limit of 60 design-time requests per minute
		time.Sleep(2 * time.Second)
	}
	return nil
}

// migrateIntents migrates intents to your CX agent
func migrateIntents(ctx context.Context) error {
	var err error

	// Create ES client
	var client2 *v2.IntentsClient
	options2 := []option.ClientOption{}
	if len(*v2Region) > 0 {
		options2 = append(options2,
			option.WithEndpoint(*v2Region+"-dialogflow.googleapis.com:443"))
	}
	client2, err = v2.NewIntentsClient(ctx, options2...)
	if err != nil {
		return err
	}
	defer client2.Close()
	var parent2 string
	if len(*v2Region) == 0 {
		parent2 = fmt.Sprintf("projects/%s/agent", *v2Project)
	} else {
		parent2 = fmt.Sprintf("projects/%s/locations/%s/agent", *v2Project, *v2Region)
	}

	// Create CX client
	var client3 *v3.IntentsClient
	options3 := []option.ClientOption{}
	if len(*v3Region) > 0 {
		options3 = append(options3,
			option.WithEndpoint(*v3Region+"-dialogflow.googleapis.com:443"))
	}
	client3, err = v3.NewIntentsClient(ctx, options3...)
	if err != nil {
		return err
	}
	defer client3.Close()
	parent3 := fmt.Sprintf("projects/%s/locations/%s/agents/%s", *v3Project, *v3Region, *v3Agent)

	// Read each V2 entity type, convert, and write to V3
	request2 := &proto2.ListIntentsRequest{
		Parent:     parent2,
		IntentView: proto2.IntentView_INTENT_VIEW_FULL,
	}
	it2 := client2.ListIntents(ctx, request2)
	for {
		var intent2 *proto2.Intent
		intent2, err = it2.Next()
		if err == iterator.Done {
			break
		}
		if err != nil {
			return err
		}
		fmt.Printf("Intent: %s\n", intent2.DisplayName)
		intent3 := convertIntent(intent2)
		if intent3 == nil {
			continue
		}

		if *dryRun {
			continue
		}

		request3 := &proto3.CreateIntentRequest{
			Parent: parent3,
			Intent: intent3,
		}
		_, err := client3.CreateIntent(ctx, request3)
		if err != nil {
			return err
		}

		// ES and CX each have a quota limit of 60 design-time requests per minute
		time.Sleep(2 * time.Second)
	}
	return nil
}

// checkFlags checks commandline flags
func checkFlags() error {
	flag.Parse()
	if len(*v2Project) == 0 {
		return fmt.Errorf("Need to supply es-project-id flag")
	}
	if len(*v3Project) == 0 {
		return fmt.Errorf("Need to supply cx-project-id flag")
	}
	if len(*v2Region) == 0 {
		fmt.Printf("No region supplied for ES, using default\n")
	}
	if len(*v3Region) == 0 {
		return fmt.Errorf("Need to supply cx-region-id flag")
	}
	if len(*v3Agent) == 0 {
		return fmt.Errorf("Need to supply cx-agent-id flag")
	}
	if len(*outFile) == 0 {
		return fmt.Errorf("Need to supply out-file flag")
	}
	return nil
}

// closeFile is used as a convenience for defer
func closeFile(f *os.File) {
	err := f.Close()
	if err != nil {
		fmt.Fprintf(os.Stderr, "ERROR closing CSV file: %v\n", err)
		os.Exit(1)
	}
}

func main() {
	if err := checkFlags(); err != nil {
		fmt.Fprintf(os.Stderr, "ERROR checking flags: %v\n", err)
		os.Exit(1)
	}
	ctx := context.Background()
	if err := migrateEntities(ctx); err != nil {
		fmt.Fprintf(os.Stderr, "ERROR migrating entities: %v\n", err)
		os.Exit(1)
	}
	if err := migrateIntents(ctx); err != nil {
		fmt.Fprintf(os.Stderr, "ERROR migrating intents: %v\n", err)
		os.Exit(1)
	}
	csvFile, err := os.Create(*outFile)
	if err != nil {
		fmt.Fprintf(os.Stderr, "ERROR opening output file: %v", err)
		os.Exit(1)
	}
	defer closeFile(csvFile)
	csvWriter := csv.NewWriter(csvFile)
	if err := csvWriter.WriteAll(issues); err != nil {
		fmt.Fprintf(os.Stderr, "ERROR writing CSV output file: %v", err)
		os.Exit(1)
	}
	csvWriter.Flush()
}

实体类型的工具迁移

Dialogflow ES 实体类型Conversational Agents (Dialogflow CX) 实体类型非常相似,因此是最容易迁移的数据类型。该工具只会原封不动地复制实体类型。

intent 的工具迁移

Dialogflow ES intentConversational Agents (Dialogflow CX) intent 截然不同。

Dialogflow ES 意图可用作代理的基础组件;它们包含训练短语、响应、对话控制上下文、webhook 配置、事件、操作和槽填充参数。

Conversational Agents (Dialogflow CX) 已将其中大部分数据移至其他资源。 Conversational Agents (Dialogflow CX) 意图仅包含训练短语和参数,这使得意图可在客服中重复使用。该工具只会将这两种 intent 数据复制到您的对话式 AI 客服 (Dialogflow CX) intent。

迁移工具限制

迁移工具不支持以下内容:

  • 超级代理:该工具无法从多个子代理读取数据,但您可以针对每个子代理多次调用该工具。
  • 多语言代理:您应修改该工具,以创建多语言训练短语和实体条目。
  • 对非英语进行系统实体验证:如果该工具发现对话式 AI 客服 (Dialogflow CX) 不支持的系统实体,则会创建 TODO 项,假定英语是默认语言,并且使用美国区域。系统实体支持因语言和区域而异。对于其他语言和区域,您应修改该工具以执行此检查。

迁移基本步骤

以下小节概述了要执行的迁移步骤。您无需按顺序执行这些手动步骤,甚至可能需要同时执行这些步骤或按其他顺序执行这些步骤。请先仔细阅读相关步骤,并开始规划更改,然后再实际进行更改。

运行迁移工具后,您可以重新构建 Conversational Agents (Dialogflow CX) 代理。您仍然需要完成大量迁移工作,但手动输入的大部分数据将显示在对话式 AI 助理 (Dialogflow CX) 代理和 TODO 文件中。

创建您的 Conversational Agents (Dialogflow CX) 客服

如果尚未创建,请创建 Conversational Agents (Dialogflow CX) 客服。请务必使用与 Dialogflow ES 代理相同的默认语言。

运行迁移工具

请按以下步骤执行该工具:

  1. 在您的计算机上安装 Go(如果您尚未安装)。
  2. 为工具代码创建一个名为 migrate 的目录。
  3. 上面的工具代码复制到此目录中名为 main.go 的文件中。
  4. 根据您的具体情况修改代码。
  5. 在此目录中创建一个 Go 模块。例如:

    go mod init migrate
    
  6. 安装 Dialogflow ES V2 和 Conversational Agents (Dialogflow CX) V3 Go 客户端库:

    go get cloud.google.com/go/dialogflow/apiv2
    go get cloud.google.com/go/dialogflow/cx/apiv3
    
  7. 确保您已设置客户端库身份验证

  8. 运行该工具,并将输出保存到文件:

    go run main.go -es-project-id=<ES_PROJECT_ID> -cx-project-id=<CX_PROJECT_ID> \
    -cx-region-id=<CX_REGION_ID> -cx-agent-id=<CX_AGENT_ID> -out-file=out.csv
    

迁移工具问题排查

如果您在运行该工具时遇到错误,请检查以下各项:

错误 解决方法
RPC 错误:训练短语部分提及了未为 intent 定义的参数。 如果您之前使用 Dialogflow ES API 创建意图参数的方式与训练短语不一致,就可能会出现这种情况。如需解决此问题,请在控制台中重命名 Dialogflow ES 参数,检查您的训练短语是否正确使用该参数,然后点击“保存”。如果您的训练短语引用了不存在的参数,也可能会出现这种情况。

修正错误后,您需要先从 Conversational Agents (Dialogflow CX) 代理中清除 intent 和实体,然后才能再次运行迁移工具。

将 Dialogflow ES intent 数据迁移到 Conversational Agents (Dialogflow CX)

该工具会将 intent 训练短语和参数迁移到对话式 AI 代理 (Dialogflow CX) intent,但还有许多其他 Dialogflow ES intent 字段需要手动迁移。

Dialogflow ES intent 可能需要相应的 Conversational Agents (Dialogflow CX) 页面、相应的 Conversational Agents (Dialogflow CX) intent,或两者兼有。

如果 Dialogflow ES 意图匹配用于将对话从特定对话节点转换到另一个节点,则您的代理中应包含与此 intent 相关的两个页面:

  • 包含意图路由的原始页面(将转换到下一个页面):原始页面中的意图路由可能包含与 Dialogflow ES 意图响应类似的 Conversational Agents (Dialogflow CX) 执行消息。您可能在此页面中有许多 intent 路线。在原始页面处于活跃状态时,这些 intent 路由可以将对话转换到许多可能的路径。许多 Dialogflow ES intent 将共用相同的对应 Conversational Agents (Dialogflow CX) 原始页面。
  • 下一页,即原始页面中 intent 路线的转换目标:下一页的 Conversational Agents (Dialogflow CX) 条目执行方式可能包含与 Dialogflow ES intent 响应类似的 Conversational Agents (Dialogflow CX) 执行方式消息。

如果 Dialogflow ES 意图包含必需参数,您应在表单中创建包含相同参数的相应对话式 AI 客服 (Dialogflow CX) 页面。

通常,Conversational Agents (Dialogflow CX) intent 和 Conversational Agents (Dialogflow CX) 页面会共用相同的参数列表,这意味着单个 Dialogflow ES intent 具有相应的 Conversational Agents (Dialogflow CX) 页面和相应的 Conversational Agents (Dialogflow CX) intent。当意图路由中带有参数的 Conversational Agents (Dialogflow CX) 意图匹配时,对话通常会转换到具有相同参数的页面。从 intent 匹配中提取的参数会传播到会话参数,这些参数可用于部分或完全填充页面表单参数。

Conversational Agents (Dialogflow CX) 中不存在后备意图和预定义的后续跟进意图。请参阅内置 intent

下表介绍了如何将 Dialogflow ES 中的特定 intent 数据映射到对话式客服 (Dialogflow CX) 资源:

Dialogflow ES intent 数据 相应的对话式 AI 客服 (Dialogflow CX) 数据 需要采取行动
训练短语 意图训练短语 通过工具迁移。该工具会检查系统实体支持,并为不受支持的系统实体创建 TODO 项。
客服人员回复 执行响应消息 请参阅代理响应
对话控制的上下文 请参阅结构和对话路径控制
网络钩子设置 执行方式 webhook 配置 请参阅webhooks
活动 流级或页面级事件处理脚本 请参阅事件
操作 履单 Webhook 代码 请参阅webhooks
参数 意图参数和/或页面表单参数 使用工具迁移到了 intent 参数。如果参数是必需的,该工具会创建 TODO 项,以便可能迁移到页面。请参阅参数
参数提示 网页表单参数提示 请参阅表单填充

创建流程

为每个高级对话主题创建一个流程。 每个流程中的主题应有所不同,以免对话频繁在流程之间跳转。

如果您使用的是超级代理,则每个子代理都应成为一个或多个流程。

从基本对话路径开始

最好在迭代更改时使用模拟器测试代理。因此,您最初应重点关注对话早期的基本对话路径,并在进行更改时进行测试。完成这些操作后,请继续构建更详细的对话路径。

流级状态处理程序与页面级状态处理程序

创建状态处理脚本时,请考虑应在流程级别还是页面级别应用这些脚本。每当流(以及流中的任何页面)处于活跃状态时,流级处理程序都处于作用范围内。只有当特定页面处于活跃状态时,页面级处理脚本才在作用范围内。流级处理程序类似于没有输入上下文的 Dialogflow ES intent。页面级处理脚本类似于包含输入上下文的 Dialogflow ES intent。

Webhook 代码

聊天机器人 (Dialogflow CX) 的 webhook 请求和响应属性有所不同。请参阅“Webhook”部分

知识连接器

Conversational Agents (Dialogflow CX) 尚不支持知识连接器。您需要将这些 intent 作为常规 intent 实现,或者等到 Conversational Agents (Dialogflow CX) 支持知识连接器。

代理设置

查看 Dialogflow ES 客服设置,并根据需要调整 Conversational Agents (Dialogflow CX) 客服设置

使用 TODO 文件

迁移工具会输出一个 CSV 文件。此列表中的项目侧重于可能需要注意的特定数据。将此文件导入电子表格。 解决电子表格中的每个项目,并使用一列标记完成情况。

API 使用情况迁移

如果您的系统使用 Dialogflow ES API 进行运行时或设计时调用,您需要更新此代码才能使用对话式代理 (Dialogflow CX) API。如果您仅在运行时使用 detect intent 调用,此更新应该非常简单。

集成

如果您的客服人员使用集成,请参阅“集成”部分,并根据需要进行更改。

以下小节概述了建议的迁移步骤。

验证

使用代理验证检查您的代理是否遵循最佳实践。

测试

执行上述手动迁移步骤时,您应使用模拟器测试代理。当代理似乎可以正常运行后,您应比较 Dialogflow ES 代理和 Conversational Agents (Dialogflow CX) 代理之间的对话,并验证行为是否类似或有所改进。

使用模拟器测试这些对话时,您应创建测试用例,以防止日后出现回归问题。

环境

检查您的 Dialogflow ES 环境,并根据需要更新 Conversational Agents (Dialogflow CX) 环境