Dialogflow ES에서 CX로 마이그레이션

Dialogflow CX 에이전트는 Dialogflow ES 에이전트보다 강력한 대화 제어 및 도구를 제공합니다. Dialogflow ES 에이전트가 복잡한 대화를 처리하는 경우 Dialogflow CX로 마이그레이션하는 것이 좋습니다.

이 가이드에서는 Dialogflow ES에서 Dialogflow CX로 에이전트를 마이그레이션하는 방법을 설명합니다. 이 두 에이전트 유형은 근본적인 차이가 있으므로 이 마이그레이션을 수행하는 간단한 방법은 없습니다.

마이그레이션에 이 가이드를 사용하는 경우 위의 의견 보내기 버튼을 클릭하여 긍정적인 의견이나 부정적인 의견을 제공하세요. 보내주신 의견은 시간이 지남에 따라 이 가이드를 개선하는 데 사용됩니다.

개략적으로 말해, 권장되는 프로세스는 자동/수동 하이브리드 프로세스입니다. 사용자는 일부 Dialogflow ES 에이전트 데이터를 읽고 해당 데이터를 Dialogflow CX 에이전트에 쓰고 TODO 목록을 캡처하는 도구를 사용하게 됩니다. 그런 다음 권장사항, TODO 목록, 도구에서 마이그레이션된 데이터를 사용하여 전체 CX 에이전트를 다시 만듭니다.

Dialogflow CX 이해

이 마이그레이션을 시도하기 전에 Dialogflow CX의 작동 방법을 확실하게 이해해야 합니다. 여기에서 시작해 보세요.

  1. 기본사항
  2. 소개 동영상
  3. 빠른 시작

또한 새 에이전트에 필요할 수 있는 기능이 있는 추가 개념 문서를 읽어야 합니다. 다음 사항을 중점적으로 알아보세요.

ES/CX 차이점 이해

이 섹션에는 Dialogflow ES와 CX의 가장 중요한 차이점이 나와 있습니다. 나중에 수동 마이그레이션 단계를 수행할 때 자세한 내용은 이 섹션을 참조하세요.

구조 및 대화 경로 제어

ES는 구조 및 대화 경로 제어를 위해 다음을 제공합니다.

  • 인텐트는 에이전트의 구성 요소로 사용됩니다. 대화의 어느 시점에서든 인텐트는 대응되며, 각 인텐트는 대화의 노드입니다.
  • 컨텍스트는 대화를 제어하는 데 사용됩니다. 컨텍스트는 언제든지 일치시킬 수 있는 인텐트를 제어하는 데 사용됩니다. 컨텍스트는 일정한 수의 대화 차례 후에 만료되므로 긴 대화에서 이러한 유형의 제어가 정확하지 않을 수 있습니다.

CX는 대화 경로의 계층 구조와 더 정확한 제어 계층 구조를 제공합니다.

  • 페이지는 대화의 그래프 노드입니다. CX 대화는 상태 머신과 비슷합니다. 대화 중 어느 시점에서든 한 페이지가 활성화됩니다. 최종 사용자 입력 또는 이벤트에 따라 대화는 다른 페이지로 전환될 수 있습니다. 페이지는 여러 차례 대화 차례 동안 활성화 상태로 유지되는 것이 일반적입니다.
  • 흐름은 관련 페이지의 그룹입니다. 각 흐름에서 대략적인 대화 주제를 처리해야 합니다.
  • 상태 핸들러는 전환 및 응답을 제어하는 데 사용됩니다. 상태 핸들러에는 다음과 같은 세 가지 유형이 있습니다.
    • 인텐트 경로: 일치되어야 하는 인텐트, 선택적 응답, 선택적 페이지 전환을 포함합니다.
    • 조건 경로: 충족해야 하는 조건, 선택적인 응답, 선택적인 페이지 전환을 포함합니다.
    • 이벤트 핸들러: 호출해야 하는 이벤트 이름, 선택적 응답, 선택적 페이지 전환을 포함합니다.
  • 범위는 상태 핸들러 호출 가능 여부를 제어하는 데 사용됩니다. 대부분의 핸들러는 페이지 또는 전체 흐름과 연결되어 있습니다. 연결된 페이지 또는 흐름이 활성 상태이면 핸들러가 범위 내에 있으며 이를 호출할 수 있습니다. 범위의 CX 인텐트 경로는 활성 상태의 입력 컨텍스트를 사용하는 ES 인텐트와 비슷합니다.

에이전트의 흐름과 페이지를 설계할 때는 에이전트 설계 가이드의 흐름 섹션의 조언을 이해해야 합니다.

양식 채우기

ES는 슬롯 채우기를 사용하여 최종 사용자로부터 필요한 매개변수를 수집합니다.

  • 이러한 매개변수는 필수로 표시된 인텐트 매개변수입니다.
  • 모든 필수 매개변수가 수집될 때까지 인텐트는 계속 일치됩니다.
  • 최종 사용자에게 값을 제공하도록 요청하는 프롬프트를 정의할 수 있습니다.

CX는 양식 채우기를 사용하여 최종 사용자로부터 필요한 매개변수를 수집합니다.

  • 이러한 매개변수는 페이지와 연결되며 페이지가 활성 상태인 동안 수집됩니다.
  • 페이지의 조건 경로를 사용하여 양식 작성이 완료되었는지 확인합니다. 이러한 조건 경로는 일반적으로 다른 페이지로 전환됩니다.
  • 프롬프트와 재요청 핸들러를 정의하여 값을 수집하려는 여러 시도를 단계적으로 처리할 수 있습니다.

전환

ES는 최종 사용자 입력이 인텐트와 일치하면 자동으로 한 인텐트에서 다음 인텐트로 전환합니다. 이 일치는 입력 컨텍스트가 없는 인텐트 또는 활성 입력 컨텍스트가 있는 인텐트에서만 발생할 수 있습니다.

범위의 상태 핸들러가 요구사항을 충족하고 전환 대상을 제공하면 CX가 한 페이지에서 다음 페이지로 전환됩니다. 이러한 전환을 사용하면 대화를 최종 사용자에게 안정적으로 안내할 수 있습니다. 이러한 전환을 제어하는 방법에는 여러 가지가 있습니다.

  • 인텐트 일치는 인텐트 경로를 트리거할 수 있습니다.
  • 조건을 충족하면 조건 경로를 트리거할 수 있습니다.
  • 이벤트를 호출하면 이벤트 핸들러가 트리거될 수 있습니다.
  • 여러 번 시도한 후 최종 사용자가 값을 제공하지 못하면 재요청 핸들러가 전환할 수 있습니다.
  • 전환 타겟에 기호 전환 타겟을 사용할 수 있습니다.

에이전트 응답

ES 에이전트 응답은 인텐트가 일치할 때 최종 사용자에게 전송됩니다.

  • 에이전트는 가능한 응답 목록에서 응답에 대한 메시지 1개를 선택할 수 있습니다.
  • 응답은 플랫폼별로 다를 수 있으며 리치 응답 형식을 사용할 수 있습니다.
  • 응답은 웹훅을 기반으로 할 수 있습니다.

CX 에이전트 응답은 fulfillment가 호출될 때 최종 사용자에게 전송됩니다. 항상 웹훅이 포함된 ES fulfillment와 달리 CX fulfillment에는 fulfillment 리소스에 웹훅이 구성되었는지 여부에 따라 웹훅 호출이 포함될 수도 있고 포함되지 않을 수도 있습니다. 웹훅 응답을 기반으로 하는 정적 응답과 동적 응답은 모두 fulfillment가 제어합니다. 에이전트 응답을 만드는 방법에는 여러 가지가 있습니다.

  • 모든 유형의 상태 핸들러에 Fulfillment를 제공할 수 있습니다.
  • 응답 큐를 통해 대화 차례 동안 여러 응답을 연결할 수 있습니다. 이 기능을 사용하면 경우에 따라 에이전트 설계를 간소화할 수 있습니다.
  • CX는 기본 제공되는 플랫폼별 응답을 지원하지 않습니다. 하지만 플랫폼별 응답에 사용할 수 있는 커스텀 페이로드를 포함하여 여러 응답 유형을 제공합니다.

매개변수

ES 매개변수의 특징은 다음과 같습니다.

  • 인텐트에만 정의됩니다.
  • 최종 사용자 입력, 이벤트, 웹훅, API 호출에 의해 설정됩니다.
  • 응답, 매개변수 프롬프트, 웹훅 코드, 매개변수 값에서 참조합니다.
    • 기본 참조 형식은 $parameter-name입니다.
    • 참조는 .original, .partial, .recent 서픽스 구문을 지원합니다.
    • 참조는 활성 컨텍스트 #context-name.parameter-name을 지정할 수 있습니다.
    • 참조는 이벤트 매개변수 #event-name.parameter-name을 지정할 수 있습니다.

CX 매개변수의 특징은 다음과 같습니다.

  • 인텐트 및 페이지 양식에 정의됩니다.
  • 인텐트 및 양식 매개변수는 세션 매개변수로 전파됩니다. 이 매개변수는 세션 기간 동안 참조할 수 있습니다.
  • 최종 사용자 입력, 웹훅, fulfillment 매개변수 사전 설정, API 호출로 설정됩니다.
  • 응답, 매개변수 프롬프트, 재요청 핸들러, 매개변수 사전 설정, 웹훅 코드에서 참조됩니다.
    • 참조 형식은 세션 매개변수의 경우 $session.params.parameter-id, 인텐트 매개변수의 경우 $intent.params.parameter-id입니다.
    • 인텐트 매개변수 참조는 .original.resolved 서픽스 구문을 지원합니다. 세션 매개변수는 이 구문을 지원하지 않습니다.

시스템 개체

ES는 여러 시스템 항목을 지원합니다.

CX는 동일한 시스템 항목을 많이 지원하지만 몇 가지 차이점이 있습니다. 마이그레이션할 때 ES에서 사용 중인 시스템 항목이 CX에서 동일한 언어로 지원되는지 확인합니다. 그렇지 않은 경우 이에 대한 커스텀 항목을 만들어야 합니다.

이벤트

ES 이벤트는 다음과 같은 특성을 갖습니다.

  • 인텐트와 일치하도록 API 호출 또는 웹훅에서 호출할 수 있습니다.
  • 매개변수를 설정할 수 있습니다.
  • 적은 수의 이벤트가 통합 플랫폼에 의해 호출됩니다.

CX 이벤트의 특징은 다음과 같습니다.

  • API 호출 또는 웹훅에서 호출하여 이벤트 핸들러를 호출할 수 있습니다.
  • 매개변수를 설정할 수 없습니다.
  • 많은 기본 제공 이벤트는 최종 사용자 입력 부족, 인식되지 않은 최종 사용자 입력, 웹훅에서 무효화한 매개변수, 웹훅 오류 처리를 위해 사용될 수 있습니다.
  • 호출은 다른 상태 핸들러와 동일한 범위 지정 규칙으로 제어할 수 있습니다.

내장 인텐트

ES는 다음과 같은 내장 인텐트를 지원합니다.

다음은 내장 인텐트의 CX 지원을 설명합니다.

  • 시작 인텐트가 지원됩니다.
  • 대체 인텐트는 제공되지 않습니다. 대신 이벤트 핸들러에서 no-match 이벤트를 사용하세요.
  • 제외 예시는 기본 제외 인텐트를 사용하세요.
  • 사전 정의된 후속 조치 인텐트는 제공되지 않습니다. 에이전트에서 필요에 따라 이러한 인텐트를 만들어야 합니다. 예를 들어 에이전트 질문에 대한 부정적인 답변(아니요, '아니요', '아니요' 등)을 처리하기 위한 인텐트를 만들어야 할 수 있습니다. CX 인텐트는 에이전트에서 재사용 가능하므로 한 번만 정의하면 됩니다. 이러한 공통 인텐트에 서로 다른 인텐트 경로를 사용하면 다양한 범위의 대화를 더 효과적으로 제어할 수 있습니다.

웹훅

ES 웹훅의 특징은 다음과 같습니다.

  • 에이전트에 웹훅 서비스를 구성할 수 있습니다.
  • 각 인텐트는 웹훅을 사용하여 표시될 수 있습니다.
  • 웹훅 오류 처리는 기본적으로 지원되지 않습니다.
  • 인텐트 작업 또는 인텐트 이름은 웹훅이 호출한 에이전트의 위치를 확인하는 데 사용됩니다.
  • 콘솔은 인라인 편집기를 제공합니다.

CX 웹훅의 특징은 다음과 같습니다.

  • 에이전트에 여러 웹훅 서비스를 구성할 수 있습니다.
  • 각 fulfillment는 선택적으로 웹훅 호출을 지정할 수 있습니다.
  • 웹훅 오류 처리는 기본적으로 지원됩니다.
  • CX fulfillment 웹훅에는 태그가 포함되어 있습니다. 이 태그는 ES 작업과 비슷하지만 웹훅을 호출할 때만 사용됩니다. 웹훅 서비스는 이러한 태그를 사용하여 에이전트에서 호출되는 위치를 결정할 수 있습니다.
  • 콘솔에는 기본 제공되는 웹훅 코드 편집기가 없습니다. Cloud Functions를 사용하는 것이 일반적이지만 여러 가지 옵션이 있습니다.

CX로 마이그레이션할 때는 요청 및 응답 속성이 다르므로 웹훅 코드를 변경해야 합니다.

통합

ES 통합CX 통합은 서로 다른 플랫폼을 지원합니다. 두 에이전트 유형 모두에서 지원되는 플랫폼의 경우 구성에 차이가 있을 수 있습니다.

사용 중인 ES 통합이 CX에서 지원되지 않는 경우 플랫폼을 전환하거나 통합을 직접 구현해야 할 수 있습니다.

CX 전용 기능 더보기

CX에서만 제공되는 기타 기능이 많습니다. 마이그레이션 중에는 이러한 기능을 사용하는 것이 좋습니다. 예를 들면 다음과 같습니다.

권장사항

마이그레이션하기 전에 CX 에이전트 설계 권장사항을 숙지하세요. 이러한 CX 권장사항의 대부분은 ES 권장사항과 비슷하지만 CX에 고유한 권장사항도 있습니다.

마이그레이션 도구 정보

마이그레이션 도구는 대부분의 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 인텐트CX 인텐트는 매우 다릅니다.

ES 인텐트는 에이전트의 구성 요소로 사용되며 학습 문구, 응답, 대화 제어 컨텍스트, 웹훅 구성, 이벤트, 작업, 슬롯 채우기 매개변수를 포함합니다.

Dialogflow CX는 이 데이터의 대부분을 다른 리소스로 이동했습니다. CX 인텐트에는 학습 문구와 매개변수만 있으므로 인텐트를 에이전트 간에 재사용할 수 있습니다. 이 도구는 두 가지 유형의 인텐트 데이터만 CX 인텐트에 복사합니다.

마이그레이션 도구 제한사항

마이그레이션 도구는 다음을 지원하지 않습니다.

  • 메가 에이전트: 도구는 여러 하위 에이전트에서 읽을 수 없지만 각 하위 에이전트에 대해 도구를 여러 번 호출할 수 있습니다.
  • 다국어 에이전트: 다국어 학습 문구와 항목 항목을 만들도록 도구를 수정해야 합니다.
  • 영어가 아닌 언어의 시스템 항목 확인: 이 도구는 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 오류입니다. 이 경우는 이전에 ES API를 사용하여 학습 문구와 일치하지 않는 방식으로 인텐트 매개변수를 만든 경우 발생할 수 있습니다. 이 문제를 해결하려면 콘솔에서 ES 매개변수의 이름을 변경하고 학습 문구에서 매개변수를 올바르게 사용하고 있는지 확인한 다음 저장을 클릭합니다. 학습 문구가 존재하지 않는 매개변수를 참조하는 경우에도 이 문제가 발생할 수 있습니다.

오류를 해결한 후 마이그레이션 도구를 다시 실행하기 전에 인텐트 및 항목의 CX 에이전트를 삭제해야 합니다.

ES 인텐트 데이터를 CX로 이동

이 도구는 인텐트 학습 문구와 매개변수를 CX 인텐트로 마이그레이션하지만 다른 많은 ES 인텐트 필드는 수동으로 마이그레이션합니다.

ES 인텐트에는 해당 CX 페이지, 해당 CX 인텐트 또는 둘 다 필요할 수 있습니다.

ES 인텐트 일치가 대화를 특정 대화 노드에서 다른 대화 노드로 전환하는 데 사용되는 경우 이 인텐트와 관련된 에이전트에 두 페이지가 있어야 합니다.

  • 다음 페이지로 전환되는 인텐트 경로가 포함된 원본 페이지: 원본 페이지의 인텐트 경로에는 ES 인텐트 응답과 비슷한 CX fulfillment 메시지가 있을 수 있습니다. 이 페이지에는 인텐트 경로가 여러 개 있을 수 있습니다. 원본 페이지가 활성 상태인 동안 이러한 인텐트 경로는 대화를 가능한 여러 경로로 전환할 수 있습니다. 많은 ES 인텐트는 동일한 해당 CX 원본 페이지를 공유합니다.
  • 다음 페이지(원본 페이지에서 인텐트 경로의 전환 타겟): 다음 페이지의 CX 항목 fulfillment에는 ES 인텐트 응답과 유사한 CX fulfillment 메시지가 있을 수 있습니다.

ES 인텐트에 필수 매개변수가 포함된 경우 양식에 동일한 매개변수가 있는 해당 CX 페이지를 만들어야 합니다.

CX 인텐트와 CX 페이지에서는 동일한 매개변수 목록을 공유하는 것이 일반적입니다. 즉, 단일 ES 인텐트에 해당하는 CX 페이지와 해당 CX 인텐트가 있는 것입니다. 인텐트 경로에 매개변수가 있는 CX 인텐트가 일치하면 대화는 동일한 매개변수가 있는 페이지로 전환되는 경우가 많습니다. 인텐트 일치에서 추출된 매개변수는 양식 매개변수로 전파됩니다. 이 매개변수는 페이지 양식 매개변수 일부 또는 전부를 사용할 수 있습니다.

대체 인텐트와 사전 정의된 후속 조치 인텐트는 CX에 존재하지 않습니다. 기본 제공 인텐트를 참조하세요.

다음 표에서는 특정 인텐트 데이터를 ES에서 CX 리소스에 매핑하는 방법을 설명합니다.

ES 인텐트 데이터 해당 CX 데이터 필요한 조치
학습 문구 인텐트 학습 문구 도구로 마이그레이션됨 도구에서 시스템 항목 지원을 확인하고 지원되지 않는 시스템 항목의 TODO 항목을 만듭니다.
에이전트 응답 fulfillment 응답 메시지 에이전트 응답을 참조하세요.
대화 제어 컨텍스트 없음 구조 및 대화 경로 제어를 참조하세요.
웹훅 설정 Fulfillment 웹훅 구성 웹훅을 참조하세요.
이벤트 흐름 수준 또는 페이지 수준 이벤트 핸들러 이벤트를 참조하세요.
작업 Fulfillment 웹훅 태그 웹훅을 참조하세요.
매개변수 인텐트 매개변수 또는 페이지 양식 매개변수 도구별로 인텐트 매개변수로 마이그레이션됨 매개변수가 필요한 경우 도구에서 페이지로 이전할 TODO 항목을 만듭니다. 매개변수를 참조하세요.
매개변수 프롬프트 페이지 양식 매개변수 프롬프트 양식 작성을 참조하세요.

흐름 만들기

상위 수준 대화 주제별로 흐름을 만듭니다. 대화가 흐름 간에 자주 전환되지 않도록 각 흐름의 주제는 고유해야 합니다.

메가 에이전트를 사용하는 경우 각 하위 에이전트는 하나 이상의 흐름이 되어야 합니다.

기본 대화 경로로 시작하기

변경사항을 반복하는 동안 시뮬레이터로 에이전트를 테스트하는 것이 가장 좋습니다. 따라서 처음에는 대화의 기본 대화 경로에 집중하고 항목을 변경할 때 테스트해야 합니다. 이것이 작동하면 보다 자세한 대화 경로로 이동합니다.

흐름 수준 및 페이지 수준 상태 핸들러 비교

상태 핸들러를 만들 때 흐름 수준 또는 페이지 수준에서 적용해야 하는지 여부를 고려하세요. 흐름 수준 핸들러는 흐름(및 흐름 내 모든 페이지)이 활성 상태일 때마다 범위 내에 있습니다. 페이지 수준 핸들러는 특정 페이지가 활성 상태일 때만 범위 내에 있습니다. 흐름 수준 핸들러는 입력 컨텍스트가 없는 ES 인텐트와 비슷합니다. 페이지 수준 핸들러는 입력 컨텍스트가 있는 ES 인텐트와 비슷합니다.

웹훅 코드

웹훅 요청 및 응답 속성은 CX에서 다릅니다. 웹훅 섹션을 참조하세요.

지식 커넥터

CX는 아직 지식 커넥터를 지원하지 않습니다. 이를 일반 인텐트로 구현하거나 Dialogflow CX가 지식 커넥터를 지원할 때까지 기다려야 합니다.

에이전트 설정

ES 에이전트 설정을 검토하고 필요에 따라 CX 에이전트 설정을 조정합니다.

TODO 파일 활용

이전 도구가 CSV 파일을 출력합니다. 이 목록의 항목은 주의가 필요할 수 있는 특정 데이터 부분에 중점을 둡니다. 이 파일을 스프레드시트로 가져옵니다. 완료로 표시할 열을 사용하여 스프레드시트의 각 항목을 확인합니다.

API 사용량 마이그레이션

시스템에서 런타임 또는 설계 시간 호출에 ES API를 사용하는 경우 CX API를 사용하도록 이 코드를 업데이트해야 합니다. 런타임 시 인텐트 인식 호출만 사용하는 경우 이 업데이트는 매우 직관적이어야 합니다.

통합

에이전트에서 통합을 사용하는 경우 통합 섹션을 참조하고 필요에 따라 변경하세요.

다음 하위 섹션에서는 권장되는 마이그레이션 단계를 간략하게 설명합니다.

검증

에이전트 유효성 검사를 사용하여 에이전트가 권장사항을 따르는지 확인합니다.

테스트

위의 수동 마이그레이션 단계를 수행하는 동안 시뮬레이터로 에이전트를 테스트해야 합니다. 에이전트가 작동하는 것으로 보이면 ES와 CX 에이전트 간의 대화를 비교하고 동작이 유사하거나 개선되는지 확인해야 합니다.

시뮬레이터로 이러한 대화를 테스트하는 동안 향후 회귀를 방지하기 위해 테스트 사례를 만들어야 합니다.

환경

ES 환경을 검토하고 필요에 따라 CX 환경을 업데이트하세요.