Dialogflow ES から CX への移行

Dialogflow CX エージェントは、Dialogflow ES エージェントよりも強力な会話制御とツールを提供します。 Dialogflow ES エージェントが複雑な会話を処理する場合は、Dialogflow CX への移行を検討する必要があります。

このガイドでは、Dialogflow ES から Dialogflow CX にエージェントを移行する方法について説明します。 この 2 つのエージェント タイプには多くの基本的な違いがあるため、この移行を実行する簡単な方法はありません。

移行にこのガイドを使用する場合は、上の [フィードバックを送信] ボタンをクリックして肯定的なフィードバックまたは否定的なフィードバックをお送りください。 お寄せいただいたフィードバックは、今後のガイドの改善に活用いたします。

大まかには、自動 / 手動のハイブリッド プロセスが推奨されます。Dialogflow ES エージェント データを読み取り、そのデータを Dialogflow CX エージェントに書き込み、TODO リストをキャプチャするツールを使用します。 次に、ベスト プラクティス、TODO リスト、ツールによって移行されたデータを使用して、CX エージェント全体を再作成します。

Dialogflow CX について

この移行を試す前に、Dialogflow CX の仕組みをしっかり理解しておく必要があります。 ここから開始できます。

  1. 基本
  2. 紹介動画
  3. クイックスタート

また、新しいエージェントで必要になる可能性のある機能を持つ追加のコンセプト ドキュメントも読む必要があります。 次の点に注力しましょう。

ES / CX の違いについて

このセクションでは、Dialogflow ES と CX の最も重要な違いについて説明します。 後で手動の移行手順を実行する場合は、このセクションを参照してください。

構造と会話パスの制御

ES では、構造と会話パスの制御用に以下が用意されています。

  • インテントは、エージェントの構成要素として使用されます。 会話のどの時点でも、インテントが一致します。ある意味では、各インテントは会話のノードです。
  • コンテキストは会話を制御するために使用されます。 コンテキストは、どのインテントをいつでも一致させることができるかを制御するために使用されます。 コンテキストは、一定数の会話ターン後に期限切れになるため、このタイプの会話は長い会話では不正確になる場合があります。

CX は、構造リソースの階層と、会話パスのより正確な制御を提供します。

  • ページは会話のグラフノードです。 CX の会話はステートマシンに似ています。 会話の任意の時点で、1 つのページがアクティブになります。 エンドユーザーの入力またはイベントに基づいて、会話が別のページに遷移することがあります。 一般的に、ページは複数の会話ターンでアクティブのままです。
  • フローは、関連するページのグループです。 各フローは、大まかな会話トピックを処理する必要があります。
  • 状態ハンドラは、遷移とレスポンスの制御に使用されます。状態ハンドラには次の 3 種類があります。
    • インテント ルート: 一致させるインテント、オプションのレスポンス、ページ遷移(オプション)が含まれます。
    • 条件ルート: 満たす必要がある条件、オプションのレスポンス、オプションのページの遷移が含まれます。
    • イベント ハンドラ: 呼び出す必要があるイベント名、オプションのレスポンス、省略可能なページ遷移が含まれます。
  • スコープ は、状態ハンドラを呼び出すことができるかどうかを制御するために使用されます。 ほとんどのハンドラは、ページまたはフロー全体に関連付けられます。 関連付けられているページまたはフローがアクティブな場合、ハンドラはスコープ内にあり、呼び出すことができます。 スコープ内の CX インテント ルートは、アクティブな入力コンテキストを持つ ES インテントに似ています。

エージェントのフローとページを設計する際は、エージェント設計ガイドのフロー セクションのアドバイスを確認してください。

フォーム入力

ES は、スロット入力を使用して、エンドユーザーから必要なパラメータを収集します。

  • これらのパラメータは、必須としてマークされたインテント パラメータです。
  • 必要なパラメータがすべて収集されるまで、インテントは一致し続けます。
  • エンドユーザーに値の入力を求めるプロンプトを定義できます。

CX は、フォーム入力を使用してエンドユーザーから必要なパラメータを収集します。

  • これらのパラメータはページに関連付けられており、ページがアクティブなときに収集されます。
  • ページ用の条件ルートを使用して、フォーム入力が完了していることを確認します。 これらの条件ルートは通常、別のページに遷移します。
  • 値の収集の試行を何回も行うために、プロンプトだけでなく、リプロンプト ハンドラを定義することもできます。

遷移

エンドユーザー入力がインテントと一致すると、ES はあるインテントから次のインテントに自動的に遷移します。 この一致は、入力コンテキストを持たないインテントまたはアクティブな入力コンテキストを持つインテントに対してのみ行われます。

スコープ内の状態ハンドラが要件を満たし、遷移ターゲットを提供すると、CX はあるページから次のページに遷移します。 これらの遷移を使用することで、会話を確実にエンドユーザーに導くことができます。 これらの遷移を制御する方法は複数あります。

  • インテント マッチングによって、インテント ルートをトリガーすることができます。
  • 条件を満たすことによって、条件ルートをトリガーすることができます。
  • イベントの呼び出しにより、イベント ハンドラをトリガーできます。
  • リプロンプト ハンドラにより、エンドユーザーが複数回試行した後に値を提供できない場合に遷移を行うことができます。
  • 遷移ターゲットにはシンボリック遷移ターゲットを使用できます。

エージェント レスポンス

インテントが一致すると、ES エージェント レスポンスがエンドユーザーに送信されます。

  • エージェントは、使用可能なレスポンスのリストから、レスポンスに対して 1 つのメッセージを選択できます。
  • レスポンスは、リッチ レスポンス形式を使用できるプラットフォーム固有でもかまいません。
  • レスポンスは Webhook によって駆動できます。

フルフィルメントが呼び出されると、CX エージェント レスポンスがエンドユーザーに送信されます。 常に Webhook を含む ES フルフィルメントとは異なり、CX フルフィルメントは、フルフィルメント リソースに Webhook が構成されているかどうかに応じて、Webhook を呼び出す場合とされない場合があります。 Webhook レスポンスに基づく静的レスポンスと動的レスポンスは、どちらもフルフィルメントによって制御されます。 エージェント レスポンスを作成するには、いくつかの方法があります。

  • フルフィルメントは、任意のタイプの状態ハンドラに指定できます。
  • 複数のレスポンスは、レスポンス キューを介して会話ターン中に連結できます。 この機能によって、エージェント設計が簡素化される場合があります。
  • CX は、組み込みのプラットフォーム固有のレスポンスをサポートしていません。 ただし、プラットフォーム固有のレスポンスに使用できるカスタム ペイロードを含む複数のレスポンス タイプが用意されています。

パラメータ

ES パラメータには次の特徴があります。

  • インテントでのみ定義されます。
  • エンドユーザー入力、イベント、Webhook、API 呼び出しによって設定されます。
  • レスポンス、パラメータ プロンプト、Webhook コード、パラメータ値で参照されます。
    • 基本的な参照形式は $parameter-name です。
    • 参照は、.original.partial.recent の接尾辞構文をサポートしています。
    • 参照には、アクティブなコンテキスト #context-name.parameter-name を指定できます。
    • 参照には、イベント パラメータ #event-name.parameter-name を指定できます。

CX パラメータには次の特徴があります。

  • インテントとページフォームで定義されます。
  • インテント パラメータとフォーム パラメータはセッション パラメータに伝播され、セッション継続時間を参照できます。
  • エンドユーザー入力、Webhook、フルフィルメント パラメータ プリセット、API 呼び出しによって設定されます。
  • レスポンス、パラメータ プロンプト、リプロンプト ハンドラ、パラメータ プリセット、Webhook コードで参照されます。
    • 参照形式は、セッション パラメータの場合は $session.params.parameter-id、インテント パラメータの場合は $intent.params.parameter-id です。
    • インテント パラメータ参照は、.original.resolved の接尾辞構文をサポートしています。 セッション パラメータは、この構文をサポートしていません。

システム エンティティ

ES は多くのシステム エンティティをサポートしています。

CX は同じシステム エンティティの多くをサポートしていますが、いくつか違いがあります。 移行する際に、ES で使用しているシステム エンティティが同じ言語の CX でもサポートされていることを確認します。 そうでない場合は、カスタム エンティティを作成する必要があります。

イベント

ES イベントには次の特徴があります。

  • インテントをマッチングするために API 呼び出しまたは Webhook から呼び出すことができます。
  • パラメータを設定できます。
  • 少数のイベントが統合プラットフォームによって呼び出されます。

CX イベントには次の特徴があります。

  • イベント ハンドラを呼び出すために、API 呼び出しまたは Webhook から呼び出すことができます。
  • パラメータを設定できません。
  • 多くの組み込みイベントは、エンドユーザー入力の不足、認識されないエンドユーザー入力、Webhook によって無効化されたパラメータ、Webhook エラーの処理に使用できます。
  • 呼び出しは、他の状態ハンドラと同じスコープルールによって制御できます。

組み込みインテント

ES では、次の組み込みインテントがサポートされています。

CX がサポートする組み込みインテントは次のとおりです。

  • ウェルカム インテントがサポートされています。
  • フォールバック インテントは提供されません。 代わりに、イベント ハンドラで no-match イベントを使用します。
  • ネガティブ サンプルについては、デフォルトのネガティブ インテントを使用します。
  • 事前定義されたフォローアップ インテントは提供されません。 これらのインテントは、エージェントの要件に従って作成する必要があります。 たとえば、エージェントの質問に対する否定的な回答(「no」、「no thanks」、「no I don't」など)を処理するインテントを作成することが必要な場合があります。 CX インテントはエージェント間で再利用可能であるため、定義する必要があるのは 1 回だけです。 これらの一般的なインテントに対して、異なるインテント ルートを異なるスコープで使用すると、会話をより詳細に制御できます。

Webhook

ES Webhook には次の特徴があります。

  • エージェントに Webhook サービスを 1 つ構成できます。
  • 各インテントは Webhook を使用してマークできます。
  • Webhook エラーを処理する組み込みサポートはありません。
  • インテント アクションまたはインテント名は、Webhook がエージェントのどこから呼び出されたかを判断するために使用されます。
  • コンソールには、インライン エディタが用意されています。

CX Webhook には次の特徴があります。

  • エージェントに複数の Webhook サービスを構成できます。
  • 各フルフィルメントは、オプションで Webhook 呼び出しを指定できます。
  • Webhook エラー処理のサポートが組み込まれています。
  • CX フルフィルメント Webhook にはタグが含まれています。 このタグは ES アクションに似ていますが、Webhook を呼び出す場合にのみ使用されます。 Webhook サービスは、これらのタグを使用して、エージェントを呼び出した場所を特定できます。
  • コンソールには、組み込みの Webhook コードエディタはありません。 Cloud Functions を使用するのが一般的ですが、さまざまなオプションがあります。

CX に移行する場合、リクエストとレスポンスのプロパティが異なるため、Webhook コードを変更する必要があります。

統合

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 インテントはエージェントの構成要素として使用され、トレーニング フレーズ、レスポンス、会話制御のコンテキスト、Webhook 構成、イベント、アクション、スロット入力パラメータが含まれています。

Dialogflow CX では、このデータの大部分が他のリソースに移動されています。CX インテントにはトレーニング フレーズとパラメータのみがあるため、エージェント間でインテントを再利用できます。 このツールは、これら 2 種類のインテント データのみを 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 インテントの一致を使用して会話を特定の会話ノードから別の会話ノードに遷移する場合、このインテントに関連する 2 つのページがエージェントに存在する必要があります。

  • 次のページに遷移するインテント ルートを含む元のページ: 元のページのインテント ルートに、ES インテントのレスポンスと同様の CX フルフィルメント メッセージが含まれている場合があります。 このページには多くのインテント ルートが存在する場合があります。 元のページがアクティブな間、これらのインテント ルートは会話を多くの可能なパスに遷移できます。 多くの ES インテントは、同じ対応する CX オリジナル ページを共有します。
  • 元のページのインテント ルートの遷移ターゲットである次のページ: 次のページの CX エントリ フルフィルメントに、ES インテントのレスポンスと同様の CX フルフィルメント メッセージが含まれている場合があります。

ES インテントに必要なパラメータが含まれている場合は、フォームで同じパラメータを使用して対応する CX ページを作成する必要があります。

CX インテントと CX ページでは同じパラメータ リストを共有するのが一般的です。つまり、1 つの ES インテントに対応する CX ページと対応する CX インテントがあることを意味します。 インテント ルートのパラメータを持つ CX インテントが一致すると、会話は同じパラメータを持つページに遷移することがよくあります。 インテント一致から抽出されたパラメータはセッション パラメータに伝播され、ページフォーム パラメータの一部または全体で使用できます。

フォールバック インテントと事前定義されたフォローアップ インテントは CX に存在しません。 組み込みインテントをご覧ください。

次の表は、特定のインテント データを ES から CX リソースにマッピングする方法を示しています。

ES インテント データ 対応する CX データ 必要な対応
トレーニング フレーズ インテント トレーニング フレーズ ツールにより移行されます。ツールはシステム エンティティのサポートを確認し、サポートされていないシステム エンティティ用に TODO 項目を作成します。
エージェント レスポンス フルフィルメントのレスポンス メッセージ エージェントのレスポンスをご覧ください。
会話制御のコンテキスト なし 構造と会話パスの制御をご覧ください。
Webhook 設定 フルフィルメント Webhook の構成 Webhook をご覧ください。
イベント フローレベルまたはページレベルのイベント ハンドラ イベントをご覧ください。
Actions フルフィルメント Webhook タグ Webhook をご覧ください。
パラメータ インテント パラメータおよび / またはページフォーム パラメータ ツールによりインテント パラメータに移行されます。パラメータが必要な場合は、ツールによってページに移行する可能性がある TODO 項目が作成されます。パラメータをご覧ください。
パラメータ プロンプト ページフォーム パラメータのプロンプト フォーム入力をご覧ください。

フローを作成する

高レベルの会話トピックごとにフローを作成します。 各フロー内のトピックは区別して、会話がフロー間で頻繁に行ったり来たりしないようにします。

メガ エージェントを使用していた場合、各サブエージェントは 1 つ以上のフローになります。

基本的な会話パスから始める

変更を反復処理しながら、シミュレータを使用してエージェントをテストすることをおすすめします。 そのために、会話の早い段階で基本的な会話パスに焦点を当て、変更を加えるたびにテストする必要があります。 これらが機能したら、より詳細な会話パスに進みます。

フローレベルとページレベルの状態ハンドラ

状態ハンドラを作成する際は、フローレベルまたはページレベルで適用する必要があるかどうかを検討してください。 フローレベルのハンドラは、フロー(つまりフロー内のページ)がアクティブなときに常にスコープに含まれます。 ページレベルのハンドラは、特定のページがアクティブな場合にのみスコープに含まれます。 フローレベルのハンドラは、入力コンテキストを持たない ES インテントに似ています。 ページレベルのハンドラは、入力コンテキストを持つ ES インテントに似ています。

Webhook コード

CX では、Webhook のリクエストとレスポンスのプロパティが異なります。Webhook のセクションをご覧ください。

ナレッジ コネクタ

CX では、ナレッジ コネクタはまだサポートされていません。 これらを通常のインテントとして実装するか、Dialogflow CX でナレッジ コネクタがサポートされるまで待つ必要があります。

エージェントの設定

ES エージェント設定を確認し、必要に応じて CX エージェント設定を調整します。

TODO ファイルを使用する

移行ツールは CSV ファイルを出力します。 このリストの項目は、注意が必要なデータに焦点を当てています。 このファイルをスプレッドシートに読み込みます。 完了を示す列を使用して、スプレッドシートの各アイテムを解決します。

API の使用の移行

システムでランタイムまたは設計時の呼び出しに ES API を使用する場合は、CX API を使用するようにこのコードを更新する必要があります。実行時にインテント検出呼び出しのみを使用する場合は、この更新はかなりシンプルになります。

統合

エージェントで統合を使用する場合は、統合のセクションを参照して必要に応じて変更してください。

以下のサブセクションでは、推奨される移行手順の概要を説明します。

検証

エージェント検証を使用して、エージェントがベスト プラクティスに従っていることを確認します。

テスト

上記の手動移行手順を実行しながら、シミュレータを使用してエージェントをテストする必要があります。 エージェントが動作することがわかったら、ES エージェントと CX エージェントの間の会話を比較し、動作が類似しているか、改善されているかを確認する必要があります。

シミュレータを使用してこれらの会話をテストする場合は、テストケースを作成して今後の回帰を回避する必要があります。

環境

ES 環境を確認し、必要に応じて CX 環境を更新します。