前のステップで作成したビルド済みエージェントには、Webhook が必要です。このチュートリアルでは、そのシンプルさから、Webhook をホストするために Cloud Run 関数を使用しますが、Webhook サービスはその他のさまざまな方法でホストできます。この例では Go プログラミング言語も使用していますが、Cloud Run 関数でサポートされている言語を使用できます。
関数の作成
Cloud Run 関数は Google Cloud コンソールで作成できます(ドキュメントに移動して、コンソールを開きます)。このチュートリアルの関数を作成するには:
Dialogflow エージェントと関数の両方が同じプロジェクト内にあることが重要です。これが、Dialogflow が関数に安全にアクセスするための最も簡単な方法です。関数を作成する前に、Google Cloud コンソールからプロジェクトを選択します。
Cloud Run 関数の概要ページを開きます。
[関数を作成] をクリックして、次のフィールドを設定します。
- 環境: 第 1 世代
- 関数名: tutorial-telecommunications-webhook
- リージョン: エージェントにリージョンを指定した場合は、同じリージョンを使用します。
- HTTP トリガータイプ: HTTP
- URL: ここのコピーボタンをクリックして、値を保存します。Webhook を構成するときに、この URL が必要になります。
- 認証: 認証が必要です
- HTTPS が必須: チェックボックスをオンにする
[保存] をクリックします。
[次へ] をクリックします(特別なランタイム、ビルド、接続、セキュリティ設定は必要ありません)。
次のフィールドを設定します。
- ランタイム: 最新の Go ランタイムを選択します。
- ソースコード: インライン エディタ
- エントリ ポイント: HandleWebhookRequest
コードを次のように置き換えます。
package cxtwh import ( "context" "encoding/json" "fmt" "log" "net/http" "os" "strings" "cloud.google.com/go/spanner" "google.golang.org/grpc/codes" ) // client is a Spanner client, created only once to avoid creation // for every request. // See: https://cloud.google.com/functions/docs/concepts/go-runtime#one-time_initialization var client *spanner.Client func init() { // If using a database, these environment variables will be set. pid := os.Getenv("PROJECT_ID") iid := os.Getenv("SPANNER_INSTANCE_ID") did := os.Getenv("SPANNER_DATABASE_ID") if pid != "" && iid != "" && did != "" { db := fmt.Sprintf("projects/%s/instances/%s/databases/%s", pid, iid, did) log.Printf("Creating Spanner client for %s", db) var err error // Use the background context when creating the client, // but use the request context for calls to the client. // See: https://cloud.google.com/functions/docs/concepts/go-runtime#contextcontext client, err = spanner.NewClient(context.Background(), db) if err != nil { log.Fatalf("spanner.NewClient: %v", err) } } } type fulfillmentInfo struct { Tag string `json:"tag"` } type sessionInfo struct { Session string `json:"session"` Parameters map[string]interface{} `json:"parameters"` } type text struct { Text []string `json:"text"` } type responseMessage struct { Text text `json:"text"` } type fulfillmentResponse struct { Messages []responseMessage `json:"messages"` } // webhookRequest is used to unmarshal a WebhookRequest JSON object. Note that // not all members need to be defined--just those that you need to process. // As an alternative, you could use the types provided by the Dialogflow protocol buffers: // https://pkg.go.dev/google.golang.org/genproto/googleapis/cloud/dialogflow/cx/v3#WebhookRequest type webhookRequest struct { FulfillmentInfo fulfillmentInfo `json:"fulfillmentInfo"` SessionInfo sessionInfo `json:"sessionInfo"` } // webhookResponse is used to marshal a WebhookResponse JSON object. Note that // not all members need to be defined--just those that you need to process. // As an alternative, you could use the types provided by the Dialogflow protocol buffers: // https://pkg.go.dev/google.golang.org/genproto/googleapis/cloud/dialogflow/cx/v3#WebhookResponse type webhookResponse struct { FulfillmentResponse fulfillmentResponse `json:"fulfillmentResponse"` SessionInfo sessionInfo `json:"sessionInfo"` } // detectCustomerAnomaly handles same-named tag. func detectCustomerAnomaly(ctx context.Context, request webhookRequest) ( webhookResponse, error) { // Create session parameters that are populated in the response. // This example hard codes values, but a real system // might look up this value in a database. p := map[string]interface{}{ "anomaly_detect": "false", "purchase": "device protection", "purchase_amount": "12.25", "bill_without_purchase": "54.34", "total_bill": "66.59", "first_month": "January 1", } // Build and return the response. response := webhookResponse{ SessionInfo: sessionInfo{ Parameters: p, }, } return response, nil } // validatePhoneLine handles same-named tag. func validatePhoneLine(ctx context.Context, request webhookRequest) ( webhookResponse, error) { // Create session parameters that are populated in the response. // This example hard codes values, but a real system // might look up this value in a database. p := map[string]interface{}{ "domestic_coverage": "true", "phone_line_verified": "true", } // Build and return the response. response := webhookResponse{ SessionInfo: sessionInfo{ Parameters: p, }, } return response, nil } // cruisePlanCoverage handles same-named tag. func cruisePlanCoverage(ctx context.Context, request webhookRequest) ( webhookResponse, error) { // Get the existing parameter values port := request.SessionInfo.Parameters["destination"].(string) port = strings.ToLower(port) // Check if the port is covered covered := "false" if client != nil { // A Spanner client exists, so access the database. // See: https://pkg.go.dev/cloud.google.com/go/spanner#ReadOnlyTransaction.ReadRow row, err := client.Single().ReadRow(ctx, "Destinations", spanner.Key{port}, []string{"Covered"}) if err != nil { if spanner.ErrCode(err) == codes.NotFound { log.Printf("Port %s not found", port) } else { return webhookResponse{}, err } } else { // A row was returned, so check the value var c bool err := row.Column(0, &c) if err != nil { return webhookResponse{}, err } if c { covered = "true" } } } else { // No Spanner client exists, so use hardcoded list of ports. coveredPorts := map[string]bool{ "anguilla": true, "canada": true, "mexico": true, } _, ok := coveredPorts[port] if ok { covered = "true" } } // Create session parameters that are populated in the response. // This example hard codes values, but a real system // might look up this value in a database. p := map[string]interface{}{ "port_is_covered": covered, } // Build and return the response. response := webhookResponse{ SessionInfo: sessionInfo{ Parameters: p, }, } return response, nil } // internationalCoverage handles same-named tag. func internationalCoverage(ctx context.Context, request webhookRequest) ( webhookResponse, error) { // Get the existing parameter values destination := request.SessionInfo.Parameters["destination"].(string) destination = strings.ToLower(destination) // Hardcoded list of covered international monthly destinations coveredMonthly := map[string]bool{ "anguilla": true, "australia": true, "brazil": true, "canada": true, "chile": true, "england": true, "france": true, "india": true, "japan": true, "mexico": true, "singapore": true, } // Hardcoded list of covered international daily destinations coveredDaily := map[string]bool{ "brazil": true, "canada": true, "chile": true, "england": true, "france": true, "india": true, "japan": true, "mexico": true, "singapore": true, } // Check coverage coverage := "neither" _, monthly := coveredMonthly[destination] _, daily := coveredDaily[destination] if monthly && daily { coverage = "both" } else if monthly { coverage = "monthly_only" } else if daily { coverage = "daily_only" } // Create session parameters that are populated in the response. // This example hard codes values, but a real system // might look up this value in a database. p := map[string]interface{}{ "coverage": coverage, } // Build and return the response. response := webhookResponse{ SessionInfo: sessionInfo{ Parameters: p, }, } return response, nil } // cheapestPlan handles same-named tag. func cheapestPlan(ctx context.Context, request webhookRequest) ( webhookResponse, error) { // Create session parameters that are populated in the response. // This example hard codes values, but a real system // might look up this value in a database. p := map[string]interface{}{ "monthly_cost": 70, "daily_cost": 100, "suggested_plan": "monthly", } // Build and return the response. response := webhookResponse{ SessionInfo: sessionInfo{ Parameters: p, }, } return response, nil } // Define a type for handler functions. type handlerFn func(ctx context.Context, request webhookRequest) ( webhookResponse, error) // Create a map from tag to handler function. var handlers map[string]handlerFn = map[string]handlerFn{ "detectCustomerAnomaly": detectCustomerAnomaly, "validatePhoneLine": validatePhoneLine, "cruisePlanCoverage": cruisePlanCoverage, "internationalCoverage": internationalCoverage, "cheapestPlan": cheapestPlan, } // handleError handles internal errors. func handleError(w http.ResponseWriter, err error) { log.Printf("ERROR: %v", err) http.Error(w, fmt.Sprintf("ERROR: %v", err), http.StatusInternalServerError) } // HandleWebhookRequest handles WebhookRequest and sends the WebhookResponse. func HandleWebhookRequest(w http.ResponseWriter, r *http.Request) { var request webhookRequest var response webhookResponse var err error // Read input JSON if err = json.NewDecoder(r.Body).Decode(&request); err != nil { handleError(w, err) return } log.Printf("Request: %+v", request) // Get the tag from the request, and call the corresponding // function that handles that tag. tag := request.FulfillmentInfo.Tag if fn, ok := handlers[tag]; ok { response, err = fn(r.Context(), request) } else { err = fmt.Errorf("Unknown tag: %s", tag) } if err != nil { handleError(w, err) return } log.Printf("Response: %+v", response) // Send response if err = json.NewEncoder(w).Encode(&response); err != nil { handleError(w, err) return } }
[デプロイ] をクリックします。
ステータス インジケーターが関数が正常にデプロイされたことを示すまで待ちます。待機中に、デプロイしたコードを確認します。
エージェントの Webhook を構成する
Webhook がサービスとして存在するようになったため、この Webhook をエージェントに関連付けます。エージェントの Webhook を作成するには:
- Dialogflow CX Console を開きます。
- ご自身の Google Cloud プロジェクトを選択します。
- エージェントを選択します。
- [Manage] タブを選択します。
- [Webhooks] をクリックします。
- 既存の Webhook(ビルド済みエージェントによって作成されたもの)をクリックします。
- [Webhook URL] を関数の作成時に保存した Webhook URL に更新します。
- [サブタイプ] を「Standard」に設定します。
- [保存] をクリックします。
エージェントを試す
エージェントを試す準備が整いました。[Test Agent] ボタンをクリックして、シミュレータを開きます。 エージェントとの次の会話を試行します。
会話ターン | 自分 | エージェント |
---|---|---|
1 | こんにちは | こんにちは。私は仮想通信エージェントです。請求と計画に関する疑問の解決、トラブルシューティング、旅行やクルーズの計画の追加をお手伝いします。ご用件をお知らせください。 |
2 | クルーズ計画を追加したい | 検索します。登録している電話番号をお知らせください。 |
3 | 123-123-1234 | クルーズの日数をお知らせください。 |
4 | 7 | オプション 1 には、会話とテキストが含まれています。1 日あたりの費用は $[XX] です。詳しくは、[URL] をご覧ください。オプション 2 には、トーク、テキスト、データが含まれます。1 日あたりの費用は $[XX] です。詳しくは、[URL] をご覧ください。どのクルーズプランを追加しますか? |
5 | オプション 1 | 承知しました。お客様のアカウントにそのプランを追加しました。クルーズ中は、地上での滞在も必要な場合があります。どの港に停まりますか? |
6 | メキシコ | では、そちらで何日滞在しますか。 |
7 | 3 | おめでとうございます。メキシコでは地上でも無料でカバーされます。設定が完了しました。他に何かご不明な点はございませんか? |
この特定の会話は、Cruise Plan
のフローで管理されていました。
会話ターン 6 で、目的地の港として「メキシコ」を指定しました。目的地の港と日数は、Collect Port
ページの destination
および trip_duration
フォーム パラメータとしてキャプチャされます。これらのパラメータの定義は、エージェントで確認できます。
Collect Port
ページには、フォーム完了の条件ルート $page.params.status = "FINAL"
があります。2 つのフォーム パラメータが指定されると、このルートが呼び出されます。このルートは Webhook を呼び出し、Webhook に cruisePlanCoverage
タグを渡します。前述の Webhook コードを調べると、このタグは同じ名前の関数を呼び出すことがわかります。
この関数は、指定された目的地がプランの対象かどうかを判断します。 この関数は、データベースに接続するための情報が特定の環境変数に設定されているかどうかを確認します。これらの環境変数が設定されていない場合、関数はハードコードされた目的地のリストを使用します。以降の手順では、宛先のプランの対象範囲を検証するために、データベースからデータを取得するように関数の環境を変更します。
トラブルシューティング
Webhook コードにロギング ステートメントが含まれています。問題が発生した場合は、関数のログを表示してみてください。
詳細
上述の手順の詳細については、以下をご覧ください。