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 에이전트 응답이 최종 사용자에게 전송됩니다.

  • 에이전트는 가능한 응답 목록에서 응답 메시지 하나를 선택할 수 있습니다.
  • 응답은 플랫폼별로 리치 응답 형식을 사용할 수 있습니다.
  • 웹훅에서 응답을 실행할 수 있습니다.

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 지원을 설명합니다.

  • 시작 인텐트가 지원됩니다.
  • 대체 인텐트는 제공되지 않습니다. 대신 이벤트 핸들러에서 일치 항목 없음 이벤트를 사용합니다.
  • 제외 예시의 경우 기본 제외 인텐트를 사용합니다.
  • 사전 정의된 후속 조치 인텐트는 제공되지 않습니다. 에이전트 요구에 따라 이러한 인텐트를 만들어야 합니다. 예를 들어 에이전트 질문에 대한 부정적인 답변(아니요, '아니요', '아니요' 등)을 처리하기 위한 인텐트를 만들어야 할 수 있습니다. 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 에이전트를 만들지 않았다면 Dialogflow CX 에이전트를 만듭니다. ES 에이전트와 같은 기본 언어를 사용해야 합니다.

마이그레이션 도구 실행

도구를 실행하려면 다음 단계를 수행합니다.

  1. 아직 Go를 설치하지 않았으면 머신에 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 환경을 업데이트하세요.