您在先前步驟中建立的預先建構的服務需要webhook。本教學課程會使用Cloud Run 函式來代管 webhook,因為這類函式相當簡單,但您還有許多其他方法可以代管 webhook 服務。這個範例也使用 Go 程式設計語言,但您可以使用 Cloud Run 函式支援的任何語言。
建立函式
您可以使用 Google Cloud 控制台建立 Cloud Run 函式 (參閱說明文件、開啟控制台)。如要為本教學課程建立函式,請按照下列步驟操作:
請務必將 Dialogflow 服務專員和函式放在同一個專案中。這是 Dialogflow 安全存取函式最簡單的方式。建立函式前,請先從 Google Cloud 控制台選取專案。
開啟 Cloud Run 函式總覽頁面。
按一下「建立函式」,然後設定下列欄位:
- 環境:第 1 代
- 函式名稱:tutorial-telecommunications-webhook
- 地區:如果您為服務專員指定了地區,請使用相同的地區。
- HTTP 觸發條件類型:HTTP
- 網址:按一下這裡的複製按鈕,然後儲存值。設定 Webhook 時,您需要使用這個網址。
- 驗證:需要驗證
- 「必須使用 HTTPS」:已勾選
按一下 [儲存]。
按一下「Next」 (您不需要特殊的執行階段、建構作業、連線或安全性設定)。
設定下列欄位:
- Runtime:選取最新的 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 } }
按一下 [Deploy] (部署)。
等待狀態指標顯示函式已成功部署。等待期間,請檢查剛部署的程式碼。
為服務專員設定 Webhook
由於 webhook 已設為服務,您現在可以將這個 webhook 與代理程式建立關聯。如要為代理程式設定 Webhook,請按照下列步驟操作:
- 開啟 Dialogflow CX 控制台。
- 選擇 Google Cloud 專案。
- 選取代理程式。
- 選取「管理」分頁標籤。
- 按一下「Webhook」。
- 按一下現有的 webhook (由預先建構的代理程式建立)。
- 將「Webhook 網址」更新為建立函式時儲存的 webhook 網址。
- 將「子類型」設為「Standard」。
- 按一下 [儲存]。
試用服務專員
服務專員現已準備就緒,可以開始試用。按一下「Test Agent」按鈕,開啟模擬工具。請嘗試與代理人進行以下對話:
對話輪次 | 您 | 虛擬服務專員 |
---|---|---|
1 | 您好 | 您好,我是虛擬電信服務專員。我可以協助你解決帳單和行程相關問題、排解問題,以及新增旅遊和郵輪行程。今天需要什麼協助嗎? |
2 | 我想新增郵輪行程 | 我來幫你看看。你的電話號碼是? |
3 | 123-123-1234 | 你的郵輪行程有幾天? |
4 | 7 | 選項 1 包含通話和簡訊。費用為 $[XX] 美元/天。詳情請參閱這裡 [網址]。選項 2 包含通話、簡訊和數據。費用為 $[XX] 美元/天。詳情請參閱這裡 [網址]。請問要新增哪個郵輪行程? |
5 | 選項 1 | 好的,我已將該方案新增至你的帳戶。在郵輪行程中,您可能也需要在陸地上使用網路。你會停靠哪個港口? |
6 | 墨西哥 | 你會在那裡停留幾天? |
7 | 3 | 好消息!墨西哥境內提供免費的陸路運送服務。完成所有設定後,今天還有其他問題需要協助嗎? |
這個對話是由 Cruise Plan
流程管理。
在第 6 次對話中,您提供「墨西哥」做為目的地港口。目的地港口和停留天數會擷取為 Collect Port
頁面的 destination
和 trip_duration
表單參數。請瀏覽您的代理程式,查看這些參數定義。
在 Collect Port
頁面中,有一個表單完成條件路徑:$page.params.status = "FINAL"
。提供兩個表單參數後,系統就會呼叫這個路徑。這個路徑會呼叫 Webhook,並為 Webhook 提供 cruisePlanCoverage
標記。查看上述 webhook 程式碼,您會發現這個標記會觸發同名函式。
這個函式會判斷提供的目的地是否包含在計畫中。這個函式會檢查是否已設定特定環境變數,並提供連線至資料庫的資訊。如果未設定這些環境變數,函式會使用硬式編碼的目的地清單。在後續步驟中,您將變更函式的環境,讓函式從資料庫擷取資料,以便驗證目的地的企劃書涵蓋範圍。
疑難排解
Webhook 程式碼包含記錄陳述式。如果發生問題,請嘗試查看函式記錄。
更多資訊
如要進一步瞭解上述步驟,請參閱: