Dialogflow ES から会話エージェント(Dialogflow CX)に移行する

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

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

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

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

会話エージェント(Dialogflow CX)について

この移行を試す前に、会話エージェント(Dialogflow CX)の仕組みをしっかり理解しておく必要があります。まずはこちらをどうぞ。

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

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

Dialogflow ES と会話エージェント(Dialogflow CX)の違いについて

このセクションでは、Dialogflow ES と会話エージェント(Dialogflow CX)の重要な違いについて説明します。後ほど手動で移行作業を行う際は、このセクションを参照してください。

構造と会話パスの制御

Dialogflow ES には、構造と会話パスの制御のために次のものを用意しています。

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

会話エージェント(Dialogflow CX)は、構造リソースの階層と、会話パスをより正確に制御する機能を備えています。

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

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

フォーム入力

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

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

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

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

遷移

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

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

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

エージェント レスポンス

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

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

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

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

パラメータ

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

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

会話エージェント(Dialogflow CX)のパラメータには次の特徴があります。

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

システム エンティティ

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

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

イベント

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

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

会話エージェント(Dialogflow CX)イベントには次の特徴があります。

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

組み込みインテント

Dialogflow ES は、次の組み込みインテントをサポートしています。

会話エージェント(Dialogflow CX)がサポートする組み込みインテントは次のとおりです。

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

Webhook

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

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

会話エージェント(Dialogflow CX)Webhook には次の特徴があります。

  • エージェントに複数の Webhook サービスを構成できます。
  • 各フルフィルメントで、必要に応じて Webhook 呼び出しを指定できます。
  • Webhook エラー処理が組み込みでサポートされています。
  • 会話エージェント(Dialogflow CX)のフルフィルメント Webhook にはタグが含まれています。このタグは Dialogflow ES アクションに似ていますが、Webhook を呼び出す場合にのみ使用されます。Webhook サービスは、これらのタグを使用して、エージェントのどの部分から呼び出されたかを確認できます。
  • コンソールには、組み込みの Webhook コードエディタはありません。 Cloud Functions を使用するのが一般的ですが、多くのオプションがあります。

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

統合

Dialogflow ES 統合会話エージェント(Dialogflow CX)統合は、異なるプラットフォームをサポートしています。両方のエージェント タイプでサポートされているプラットフォームで、構成が異なる場合があります。

使用していた Dialogflow ES 統合が会話エージェント(Dialogflow CX)でサポートされていない場合は、プラットフォームを切り替えるか、統合を自分で実装する必要があります。

会話エージェント(Dialogflow CX)のみのその他の機能

会話エージェント(Dialogflow CX)でのみ提供される機能は他にも多数あります。移行中にこれらの機能の使用を検討してください。 次に例を示します。

ベスト プラクティス

移行する前に、会話エージェント(Dialogflow CX)エージェントの設計のベスト プラクティスを理解しておいてください。これらの会話エージェント(Dialogflow CX)のベスト プラクティスの多くは Dialogflow ES のベスト プラクティスと似ていますが、会話エージェント(Dialogflow CX)固有のものもあります。

移行ツールについて

移行ツールは、Dialogflow ES データの大部分を会話エージェント(Dialogflow 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()
}

エンティティ タイプのツール移行

Dialogflow ES エンティティ タイプ会話エージェント(Dialogflow CX)エンティティ タイプはよく似ており、移行が最も簡単なデータ型です。ツールによって、エンティティ タイプが単にそのままコピーされます。

インテントのツール移行

Dialogflow ES のインテント会話エージェント(Dialogflow CX)のインテントは大きく異なります。

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

会話エージェント(Dialogflow CX)では、このデータの大部分が他のリソースに移動されています。会話エージェント(Dialogflow CX)のインテントにはトレーニング フレーズとパラメータのみが含まれるため、エージェント間でインテントを再利用できます。ツールは、これらの 2 種類のインテント データのみを会話エージェント(Dialogflow CX)のインテントにコピーします。

移行ツールの制限事項

移行ツールは、次の項目をサポートしていません。

  • メガ エージェント: ツールは複数のサブエージェントから読み取ることはできませんが、各サブエージェントに対してツールを複数回呼び出すことができます。
  • 多言語エージェント: ツールを変更して、多言語のトレーニング フレーズとエンティティ エントリを作成する必要があります。
  • 英語以外の言語に関するシステム エンティティの確認: ツールは、会話エージェント(Dialogflow CX)でサポートされていないシステム エンティティを検出すると、TODO 項目を作成します。英語がデフォルトの言語であり、米国のリージョンを使用していることを前提としています。システム エンティティのサポートは、言語や地域によって異なります。 他の言語と地域の場合は、このチェックを実行するようにツールを変更する必要があります。

必須の移行手順

以降のサブセクションでは、移行手順の概要を説明します。これらの手動の手順は、順番に行う必要はありません。また、これらのステップを同時に、または異なる順序で実行することもできます。 実際に変更を加える前に、手順全体を読んで変更の計画を開始します。

移行ツールを実行した後、会話エージェント(Dialogflow CX)エージェントを再構築します。この段階でもかなりの量の移行作業が残っていますが、手動で入力したデータの大部分は会話エージェント(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 エラー。 これは、以前に Dialogflow ES API を使用して、トレーニング フレーズと一致しない方法でインテント パラメータを作成した場合に発生することがあります。この問題を解決するには、コンソールから Dialogflow ES パラメータの名前を変更し、トレーニング フレーズがパラメータを正しく使用していることを確認してから、[保存] をクリックします。また、トレーニング フレーズで存在しないパラメータを参照している場合にも発生することがあります。

エラーを修正したら、移行ツールを再度実行する前に、会話エージェント(Dialogflow CX)エージェントのインテントとエンティティを消去する必要があります。

Dialogflow ES のインテント データを会話エージェント(Dialogflow CX)に移動する

このツールは、インテントのトレーニング フレーズとパラメータを会話エージェント(Dialogflow CX)のインテントに移行しますが、手動で移行する必要がある Dialogflow ES インテント フィールドが他にも多くあります。

Dialogflow ES インテントには、対応する会話エージェント(Dialogflow CX)ページか対応する会話エージェント(Dialogflow CX)インテント、またはその両方が必要になる場合があります。

Dialogflow ES インテントの一致を使用して会話を特定の会話ノードから別の会話ノードに遷移させる場合、このインテントに関連する以下の 2 つのページがエージェントに存在する必要があります。

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

Dialogflow ES インテントに必須パラメータが含まれている場合は、フォームに同じパラメータを持つ対応する会話エージェント(Dialogflow CX)ページを作成する必要があります。

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

会話エージェント(Dialogflow CX)には、フォールバック インテントと事前定義されたフォローアップ インテントはありません。組み込みインテントをご覧ください。

次の表に、Dialogflow ES から会話エージェント(Dialogflow CX)リソースに特定のインテント データをマッピングする方法を示します。

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

フローを作成する

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

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

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

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

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

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

Webhook コード

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

ナレッジ コネクタ

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

エージェントの設定

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

TODO ファイルを使用する

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

API 使用の移行

システムでランタイムまたは設計時の呼び出しに Dialogflow ES API を使用している場合は、会話エージェント(Dialogflow CX)API を使用するようにこのコードを更新する必要があります。実行時に検出インテントの呼び出しのみを使用している場合は、この更新は非常に簡単です。

統合

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

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

検証

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

テスト

上記の手動移行手順を実行しながら、シミュレータを使用してエージェントをテストする必要があります。 エージェントが動作していることが確認できれば、Dialogflow ES エージェントと会話エージェント(Dialogflow CX)エージェントの間の会話を比較し、動作が同等以上になっているかを確認する必要があります。

シミュレータでこれらの会話をテストする際に、テストケースを作成して将来の回帰を防ぐ必要があります。

環境

Dialogflow ES 環境を確認し、必要に応じて会話エージェント(Dialogflow CX)環境を更新します。