从 Dialogflow ES 迁移到对话式客服 (Dialogflow CX)

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

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

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

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

了解对话代理 (Dialogflow CX)

在尝试此次迁移之前 您应该对 Conversational Agents (Dialogflow CX) 的工作原理具有深入了解。 您可以从这里开始:

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

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

了解 Dialogflow ES/对话代理 (Dialogflow CX) 的区别

本节列出了 Dialogflow Dialogflow ES 和对话代理 (Dialogflow CX)。 稍后执行手动迁移步骤时,您应参阅本部分以获取指导。

结构和对话路径控制

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

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

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

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

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

表单填充

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

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

对话代理 (Dialogflow CX) 使用 表单填充 从最终用户处收集所需参数:

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

转换

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

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

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

客服人员回复

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

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

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

  • 可以将 fulfillment 提供给任何类型的状态处理程序。
  • 在一轮对话中,您可以通过 响应队列。 在某些情况下,此功能可以简化代理设计。
  • 对话式客服 (Dialogflow CX) 不支持平台专用内置响应。不过,它提供 多种响应类型, 包括可用于针对平台特定响应的自定义载荷。

参数

Dialogflow ES 参数具有以下特征:

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

对话式客服 (Dialogflow CX) 参数具有以下特点:

  • 在 intent 和页面表单中定义。
  • 意图和表单参数会传播到会话参数,在会话期间可供引用。
  • 由最终用户输入、webhook 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 调用,以匹配 intent。
  • 可以设置参数。
  • 集成平台会调用少量事件。

对话式客服 (Dialogflow CX) 事件具有以下特点:

  • 可通过 API 调用或网络钩子调用来调用事件处理脚本。
  • 无法设置参数。
  • 许多内置事件可用于处理缺少最终用户输入、无法识别的最终用户输入、由网络钩子失效的参数以及网络钩子错误。
  • 调用可以由 范围规则 与状态处理程序类似

内置意图

Dialogflow ES 支持以下内置 intent:

以下内容介绍了对话式 AI 客服 (Dialogflow CX) 对内置 intent 的支持:

  • 欢迎 intent
  • 未提供回退 intent。 使用 非匹配 事件处理脚本中的事件。
  • 对于反例, 使用 默认否定意图
  • 不提供预定义的后续 intent。您必须根据代理的要求创建这些意图。 例如: 那么你可能需要创建一个意图 处理代理问题的负面回答 (“不”“不用了”“不,我不想”等等)。 对话代理 (Dialogflow CX) 意图可以在代理中重复使用, 因此您只需定义这些函数一次 在不同作用域中,为这些常见 intent 使用不同的 intent 路由,可让您更好地控制对话。

Webhook

Dialogflow ES Webhook 具有以下特征:

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

对话代理 (Dialogflow CX) webhook 具有以下特征:

  • 您可以为代理配置多个 webhook 服务。
  • 每个 fulfillment 都可以指定 webhook 调用。
  • 它内置了对 webhook 错误处理
  • 对话代理 (Dialogflow CX) fulfillment webhook 包含一个 tag。 此标签与 Dialogflow ES 操作类似, 但它仅在调用 webhook 时使用。 Webhook 服务可以使用这些标记来确定 在代理中调用它的位置。
  • 控制台没有内置的网络钩子代码编辑器。 通常使用 Cloud Functions 即可,但还有许多其他选项。

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

集成

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

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

更多仅限对话代理 (Dialogflow CX) 的功能

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

最佳做法

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

关于迁移工具

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

迁移工具代码

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

// 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 intent对话式客服 (Dialogflow CX) intent 非常不同。

Dialogflow ES 意图用作代理的构建块; 它们包含训练短语、响应、 进行对话控制, webhook 配置、事件、操作和槽位填充参数。

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

迁移工具限制

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

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

迁移基本步骤

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

运行迁移工具后 您可以重新构建对话代理 (Dialogflow CX) 代理。 您仍然有大量的迁移工作要做 但大部分手动输入的数据将出现在 Conversational Agents (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
    

迁移工具问题排查

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

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

修复错误后,您需要清除 Conversational Agents (Dialogflow CX) 代理中的意图和 然后才能再次运行迁移工具

将 Dialogflow ES intent 数据迁移到对话代理 (Dialogflow CX)

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

Dialogflow ES intent 可能需要相应的对话代理 (Dialogflow CX) 页面, 相应的对话代理 (Dialogflow CX) intent, 或者两者兼有

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

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

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

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

对话式客服 (Dialogflow CX) 中不存在后备意图和预定义的后续跟进意图。请参阅内置 intent

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

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

创建流

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

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

从基本对话路径开始

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

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

创建状态处理程序时, 请考虑是应该在数据流级别还是页面级别应用它们。 当数据流时,流级处理程序在作用域内 (也就是数据流中的任何页面) 有效。 仅当特定页面处于活动状态时,页面级处理程序才会在作用域内。 流级处理程序类似于没有输入上下文的 Dialogflow ES intent。页面级处理脚本类似于包含输入上下文的 Dialogflow ES intent。

网络钩子代码

对话式客服 (Dialogflow CX) 而言,webhook 请求和响应属性有所不同。请参阅“Webhook”部分

知识连接器

对话代理 (Dialogflow CX) 不支持 知识连接器 。 您需要将这些 intent 作为常规 intent 实现,或者等到对话式客服 (Dialogflow CX) 支持知识连接器。

代理设置

查看 Dialogflow ES 客服设置,并根据需要调整对话式客服 (Dialogflow CX) 客服设置

使用 TODO 文件

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

API 使用情况迁移

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

集成

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

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

验证

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

测试

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

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

环境

请查看您的 Dialogflow ES 环境 并更新您的 对话代理 (Dialogflow CX) 环境