从 Dialogflow ES 迁移到 Dialogflow CX

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

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

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

从总体上讲,建议的流程是自动化/手动混合流程。您将使用一种工具来读取部分 Dialogflow ES 代理数据,将这些数据写入 Dialogflow CX 代理,并捕获待办事项列表。然后,您可以使用最佳实践、待办事项列表以及该工具迁移的数据重新创建完整的 Dialogflow CX 代理。

了解 Dialogflow CX

在尝试进行此迁移之前,您应该对 Dialogflow CX 流的工作方式有扎实的了解。您可以从以下方面着手:

您还应阅读具有您可能需要在新代理中使用的功能的其他概念文档。 重点关注以下几点:

了解 Dialogflow ES/Dialogflow CX 的区别

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

结构和对话路径控制

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

  • 意图用作代理的基础组件。在对话中的任何时间点,系统都会匹配一个意图,从某种意义上说,每个意图都是对话的一个节点。
  • 上下文用于控制对话。上下文用于控制在任何给定时间可以匹配哪些意图。 上下文会在一定数量的对话回合后过期,因此这种类型的控制对于长时间的对话可能不太准确。

Dialogflow CX 提供了一套结构资源层次结构,可更精确地控制对话路径:

  • 页面是对话的图谱节点。Dialogflow CX 对话类似于状态机。在对话中的任何给定时间点,只有一个页面处于活动状态。 根据最终用户输入或事件,对话可能会转换到其他页面。 网页通常会在多个对话轮次中保持活跃状态。
  • 流量是指一组相关网页。 每个流都应处理一个简要对话主题。
  • 状态处理程序用于控制转换和响应。状态处理程序有三种类型:
    • 意图路由:包含必须匹配的意图、可选的响应和可选的页面转换。
    • 条件路由:包含必须满足的条件、可选的响应和可选的页面转换。
    • 事件处理脚本:包含必须调用的事件名称、可选的响应和可选的页面过渡。
  • 范围用于控制是否可以调用状态处理程序。大多数处理程序都与网页或整个流程相关联。如果关联的页面或流处于活跃状态,则处理程序在范围内,可以调用。范围内的 Dialogflow CX 意图路由类似于具有活跃输入上下文的 Dialogflow ES 意图。

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

表单填充

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

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

Dialogflow CX 使用表单填充从最终用户处收集必需参数:

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

转换

当最终用户输入与某个意图匹配时,Dialogflow ES 会自动从一个意图过渡到下一个意图。只有在以下情况下,系统才会进行此匹配:意图没有输入上下文,或者意图具有活跃的输入上下文。

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

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

智能体回答

当意图匹配时,Dialogflow ES 代理响应会发送给最终用户:

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

当调用实现时,Dialogflow CX 代理响应会发送给最终用户。与始终涉及网络钩子的 Dialogflow ES fulfillment 不同,Dialogflow CX fulfillment 可能涉及调用网络钩子,也可能不涉及,具体取决于 fulfillment 资源是否配置了网络钩子。基于网络钩子响应的静态响应和动态响应均由 fulfillment 控制。您可以通过多种方式创建代理回答:

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

参数

Dialogflow ES 参数具有以下特征:

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

Dialogflow CX 实参具有以下特征:

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

系统实体

Dialogflow ES 支持多种系统实体

Dialogflow CX 支持许多相同的系统实体,但也有一些差异。迁移时,请验证您在 Dialogflow ES 中使用的系统实体是否也受 Dialogflow CX 支持,并且支持的语言相同。如果不是,您应该为这些内容创建自定义实体。

事件

Dialogflow ES 事件具有以下特征:

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

Dialogflow CX 事件具有以下特征:

  • 可通过 API 调用或 webhook 调用,以调用事件处理脚本。
  • 无法设置参数。
  • 许多内置事件可用于处理以下情况:缺少最终用户输入、最终用户输入无法识别、参数因网络钩子而失效,以及发生网络钩子错误。
  • 调用可由与其他状态处理程序相同的范围界定规则控制。

内置意图

Dialogflow ES 支持以下内置 intent:

下文介绍了 Dialogflow CX 对内置 intent 的支持:

  • 支持欢迎 intent
  • 未提供回退 intent。 请改用事件处理脚本中的 no-match 事件。
  • 对于反例,请使用默认负意图
  • 未提供预定义的后续意图。 您必须根据代理的需要创建这些意图。 例如,您可能需要创建一个 intent 来处理对代理问题的否定回答(“否”“不用了”“我不要”等)。Dialogflow CX 意图可在整个代理中重复使用,因此您只需定义一次。 在不同范围内为这些常见 intent 使用不同的 intent 路由,可让您更好地控制对话。

网络钩子

Dialogflow ES Webhook 具有以下特征:

  • 您可以为代理配置一个 webhook 服务。
  • 每个意图都可以标记为使用 Webhook。
  • 没有内置对处理 Webhook 错误的支持。
  • Webhook 使用 intent 操作或 intent 名称来确定其在代理中的调用位置。
  • 控制台提供内嵌编辑器

Dialogflow CX Webhook 具有以下特征:

  • 您可以为代理配置多个 Webhook 服务。
  • 每个 fulfillment 都可以选择性地指定 webhook 调用。
  • 系统内置了对 Webhook 错误处理的支持。
  • Dialogflow CX fulfillment 网络钩子包含一个标记。 此标记类似于 Dialogflow ES 操作,但仅在调用 Webhook 时使用。 webhook 服务可以使用这些标记来确定其在代理中的调用位置。
  • 控制台没有内置的网络钩子代码编辑器。 Cloud Run functions 是常用的选择,但还有许多其他选项。

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

集成

Dialogflow ES 集成Dialogflow CX 集成支持不同的平台。对于两种代理类型都支持的平台,配置可能存在差异。

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

更多仅限 Dialogflow CX 的功能

Dialogflow CX 还提供许多其他功能。您应考虑在迁移时使用这些功能。 例如:

最佳做法

在迁移之前,请先熟悉 Dialogflow CX 代理设计最佳实践。这些 Dialogflow CX 最佳实践中有许多与 Dialogflow ES 最佳实践类似,但也有一些是 Dialogflow CX 特有的。

关于迁移工具

迁移工具会将大部分 Dialogflow ES 数据复制到您的 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 实体类型Dialogflow CX 实体类型非常相似,因此它们是最容易迁移的数据类型。该工具只是按原样复制实体类型。

意图的工具迁移

Dialogflow ES intentDialogflow CX intent 有很大区别。

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

Dialogflow CX 已将大部分此类数据移至其他资源。Dialogflow CX 意图仅包含训练短语和参数,这使得意图可在整个代理中重复使用。 该工具只会将这两种类型的 intent 数据复制到您的 Dialogflow CX intent 中。

迁移工具限制

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

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

基本迁移步骤

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

运行迁移工具后,您可以重新构建 Dialogflow CX 代理。您仍然需要完成相当多的迁移工作,但大部分手动输入的数据将显示在 Dialogflow CX 代理和 TODO 文件中。

创建 Dialogflow CX 代理

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

运行迁移工具

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

  1. 如果您尚未在机器上安装 Go,请先安装。
  2. 创建一个名为 migrate 的目录,用于存放工具代码。
  3. 上面的工具代码复制到此目录中名为 main.go 的文件中。
  4. 如果需要,请根据您的具体情况修改代码。
  5. 在此目录中创建 Go 模块。例如:

    go mod init migrate
    
  6. 安装 Dialogflow ES V2 和 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
    

迁移工具问题排查

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

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

修正错误后,您需要先清除 Dialogflow CX 代理的意图和实体,然后再运行迁移工具。

将 Dialogflow ES 意向数据迁移到 Dialogflow CX

该工具会将意向训练短语和参数迁移到 Dialogflow CX 意向,但还有许多其他 Dialogflow ES 意向字段需要手动迁移。

Dialogflow ES 意图可能需要相应的 Dialogflow CX 网页、相应的 Dialogflow CX 意图,或者两者都需要。

如果使用 Dialogflow ES 意图匹配将对话从特定对话节点转换到另一个节点,您应在代理中设置与此意图相关的两个网页:

  • 包含意图路由的原始页面,将转换到下一页面:原始页面中的意图路由可能包含类似于 Dialogflow ES 意图响应的 Dialogflow CX 实现消息。此页面中可能包含许多 intent 路由。 当原始页面处于活跃状态时,这些 intent 路由可以将对话转换到许多可能的路径。许多 Dialogflow ES intent 将共享同一对应的 Dialogflow CX 原始网页。
  • 下一个页面,即原始页面中意图路由的转换目标:下一个页面的 Dialogflow CX 入口履行业务可能包含与 Dialogflow ES 意图响应类似的 Dialogflow CX 履行业务消息。

如果 Dialogflow ES 意图包含必需参数,您应创建一个相应的 Dialogflow CX 页面,并在表单中包含相同的参数。

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

Dialogflow CX 中没有后备意图和预定义的后续意图。 请参阅内置 intent

下表介绍了如何将 Dialogflow ES 中的特定 intent 数据映射到 Dialogflow CX 资源:

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

创建流程

为每个简要对话主题创建一个流。 每个流程中的主题应各不相同,这样对话就不会在流程之间频繁来回跳转。

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

从基本对话路径开始

在迭代更改时,最好使用模拟器测试代理。 因此,您应在对话开始时先重点关注基本对话路径,并在进行更改时进行测试。在这些基本对话路径正常运行后,您可以开始构建更详细的对话路径。

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

创建状态处理程序时,请考虑它们应应用于流程级还是页面级。只要流(以及流中的任何页面)处于活跃状态,流级处理程序就位于范围内。只有当特定页面处于活跃状态时,页面级处理脚本才在范围内。 流级处理程序类似于没有输入上下文的 Dialogflow ES 意图。页面级处理脚本类似于具有输入上下文的 Dialogflow ES 意图。

Webhook 代码

Dialogflow CX 的 webhook 请求和响应属性有所不同。请参阅网络钩子部分

知识连接器

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

代理设置

查看您的 Dialogflow ES 代理设置,并根据需要调整 Dialogflow CX 代理设置

使用 TODO 文件

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

API 用量迁移

如果您的系统使用 Dialogflow ES API 进行运行时或设计时调用,则需要更新此代码以使用 Dialogflow CX API。如果您仅在运行时使用检测 intent 调用,则此更新应该相当简单。

集成

如果您的代理使用集成,请参阅集成部分,并根据需要进行更改。

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

验证

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

测试

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

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

环境

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