从 Dialogflow ES 迁移到 CX

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

本指南介绍如何将代理从 Dialogflow ES 迁移到 Dialogflow CX。 这两种代理类型有许多根本区别,因此没有直接的方法来执行此迁移。

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

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

了解 Dialogflow CX

在尝试此迁移之前,您应该对 Dialogflow CX 的工作原理有充分的了解。您可以从这里开始:

  1. 基础配置
  2. 简介视频
  3. 快速入门

您还应阅读其他概念文档,这些文档包含新代理中可能需要的功能。请重点注意以下几点:

了解 ES/CX 差异

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

结构和对话路径控制

ES 提供以下内容来控制结构和对话路径:

  • 意图用作代理的构建块。在对话中的任何位置,系统都会匹配一个意图,从某种意义上说,每个意图都是对话的一个节点。
  • Context 用于控制对话。 上下文用于控制在任何给定时间可以匹配哪些 intent。上下文会在对话经过一定次数后过期,因此这种类型的控制对于较长的对话可能不准确。

CX 提供了结构化资源层次结构以及更精确的对话路径控制机制:

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

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

表单填充

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

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

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

  • 这些参数与网页相关联,是在网页处于活动状态时收集的。
  • 您可以对页面使用条件路由,以确定表单填充是否已完成。这些条件路由通常会过渡到另一个页面。
  • 您可以定义提示以及重新提示处理程序,妥善处理多次尝试收集值的操作。

转换

当最终用户输入与 intent 匹配时,ES 会自动从一个 intent 转换为下一个 intent。这种匹配仅适用于没有输入上下文的意图或具有活跃输入上下文的意图。

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

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

代理响应

当意图匹配成功时,系统会将 ES 代理响应发送给最终用户:

  • 代理可以从可能的响应列表中选择一条消息作为响应。
  • 响应可以特定于平台,可以使用丰富的响应格式
  • 响应可以由 webhook 驱动。

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

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

参数

ES 参数具有以下特征:

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

CX 参数具有以下特征:

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

系统实体

ES 支持许多系统实体

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

事件

ES 事件具有以下特征:

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

CX 事件具有以下特征:

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

内置意图

ES 支持以下内置 intent:

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

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

Webhook

ES webhook 具有以下特征:

  • 您可以为代理配置一项网络钩子服务。
  • 每个 intent 都可以标记为使用 webhook。
  • 未对处理 webhook 错误提供内置支持。
  • webhook 使用 intent 操作或 intent 名称来确定在代理中的哪个位置调用 intent。
  • 控制台提供内嵌编辑器

CX 网络钩子具有以下特征:

  • 您可以为代理配置多项网络钩子服务。
  • 每个 fulfillment 都可以视情况指定一个 webhook 调用。
  • 系统内置了对 webhook 错误处理的支持。
  • CX 执行方式 webhook 包含一个标记。 此标记与 ES 操作类似,但它仅在调用 webhook 时使用。webhook 服务可以使用这些标记来确定从代理中的哪个位置调用 webhook。
  • 控制台没有内置网络钩子代码编辑器。Cloud Functions 是一种常见用法,但也有很多可供选择。

迁移到 CX 时,您需要更改 webhook 代码,因为请求和响应属性不同。

集成

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

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

更多特定于 CX 的功能

还有许多其他功能只有 CX 才能提供。您应考虑在迁移过程中使用这些功能。 例如:

最佳实践

在迁移之前,请先熟悉 CX 代理设计最佳实践。这些客户体验最佳实践中有许多类似于 ES 最佳实践,但有些是客户体验所特有的。

关于迁移工具

迁移工具会将大量 ES 数据复制到您的 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()
}

实体类型的工具迁移

ES 实体类型CX 实体类型非常相似,因此它们是最容易迁移的数据类型。该工具仅按原样复制实体类型。

意图的工具迁移

ES intentCX intent 有很大不同。

ES intent 用作代理的构建块;它们包含训练短语、响应、对话控制的上下文、网络钩子配置、事件、操作和槽位填充参数。

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

迁移工具限制

迁移工具不支持以下各项:

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

基本迁移步骤

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

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

创建 Dialogflow CX 代理

创建 Dialogflow CX 代理(如果尚未创建)。请务必使用与您的 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
    

迁移工具问题排查

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

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

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

将 ES intent 数据迁移至 CX

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

ES intent 可能需要相应的 CX 页面和/或相应的 CX intent。

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

  • 包含 intent 路由的原始页面,该页面将转换为下一页:原始页面中的 intent 路由可能具有类似于 ES intent 响应的 CX 执行方式消息。此页面中可能有许多意图路由。当原始页面处于活动状态时,这些意图路由可以将对话转换为许多可能的路径。许多 ES intent 将共用同一个对应的 CX 原始页面。
  • 下一页,即原始页面中 intent 路由的转换目标:下一页的 CX 条目执行方式可能包含与 ES intent 响应类似的 CX 执行方式消息。

如果 ES intent 包含必需参数,您应在表单中使用相同参数创建相应的 CX 页面。

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

CX 中不存在回退 intent 和预定义的后续 intent。请参阅内置 intent

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

ES intent 数据 对应的客户体验数据 需要采取行动
训练短语 意图训练短语 已由工具迁移。工具会检查系统实体支持,并为不受支持的系统实体创建 TODO 项。
代理响应 履单响应消息 请参阅代理响应
对话控制上下文 请参阅结构和对话路径控制
网络钩子设置 执行方式 webhook 配置 请参阅 webhook
事件 流程级或网页级事件处理脚本 请参阅事件
操作 执行方式网络钩子代码 请参阅 webhook
参数 intent 参数和/或网页表单参数 已按工具迁移到 intent 参数。如果必须提供这些参数,该工具会创建 TODO 项,以便可能迁移到网页。请参阅参数
参数提示 页面表单参数提示 请参阅表单填充

创建流

为每个概要对话主题创建流程。 每个流中的主题应该是不同的,以便对话不会在流之间频繁地来回切换。

如果您使用的是超级代理,则每个分代理应该变成一个或多个流。

从基本对话路径开始

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

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

创建状态处理程序时,请考虑它们应该在流程级别还是页面级别应用。只要流(以及流内的任何页面)处于活动状态,流级处理程序就在作用域内。仅当特定网页处于活动状态时,网页级处理程序才位于作用域内。流级处理程序类似于没有输入上下文的 ES intent。页面级处理程序类似于具有输入上下文的 ES intent。

网络钩子代码

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

知识连接器

CX 尚不支持知识连接器。 您需要作为常规 intent 来实现这些目的,或者等到 Dialogflow CX 支持知识连接器。

代理设置

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

利用 TODO 文件

迁移工具会输出一个 CSV 文件。此列表中的项目主要关注可能需要注意的特定数据。 将此文件导入电子表格。 解析电子表格中的每项内容,使用一列标记为已完成。

API 使用迁移

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

集成

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

以下小节概述了推荐的迁移步骤。

验证

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

测试

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

使用模拟器测试这些对话时,您应该创建测试用例,以防止未来再次发生回归。

环境

查看您的 ES 环境并根据需要更新 CX 环境