建立 Webhook 服務

您在上一個步驟中建立的預先建構代理程式無法提供帳戶餘額等動態資料,因為所有內容都已硬式編碼至代理程式。在本教學課程的這個步驟中,您將建立webhook,以便向服務機器人提供動態資料。本教學課程會使用Cloud Run 函式來代管 webhook,因為這類函式相當簡單,但您還有許多其他方法可以代管 webhook 服務。這個範例也使用 Go 程式設計語言,但您可以使用 Cloud Run 函式支援的任何語言

建立函式

您可以使用 Google Cloud 控制台建立 Cloud Run 函式 (參閱說明文件開啟控制台)。如要為本教學課程建立函式,請按照下列步驟操作:

  1. 請務必將 Dialogflow 服務專員和函式放在同一個專案中。這是 Dialogflow 安全存取函式最簡單的方式。建立函式前,請先從 Google Cloud 控制台選取專案。

    前往專案選取器

  2. 開啟 Cloud Run 函式總覽頁面。

    前往 Cloud Run 函式總覽

  3. 按一下「建立函式」,然後設定下列欄位:

    • 環境:第 1 代
    • 函式名稱:tutorial-banking-webhook
    • 地區:如果您為服務專員指定了地區,請使用相同的地區。
    • HTTP 觸發條件類型:HTTP
    • 網址:按一下這裡的複製按鈕,然後儲存值。設定 Webhook 時,您需要使用這個網址。
    • 驗證:需要驗證
    • 「必須使用 HTTPS」:已勾選
  4. 按一下 [儲存]

  5. 按一下「Next」 (您不需要特殊的執行階段、建構作業、連線或安全性設定)。

  6. 設定下列欄位:

    • Runtime:選取最新的 Go 執行階段。
    • 原始碼:內嵌編輯器
    • 進入點:HandleWebhookRequest
  7. 將程式碼替換為以下內容:

    package estwh
    
    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 queryResult struct {
    	Action     string                 `json:"action"`
    	Parameters map[string]interface{} `json:"parameters"`
    }
    
    type text struct {
    	Text []string `json:"text"`
    }
    
    type message struct {
    	Text text `json:"text"`
    }
    
    // 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://godoc.org/google.golang.org/genproto/googleapis/cloud/dialogflow/v2#WebhookRequest
    type webhookRequest struct {
    	Session     string      `json:"session"`
    	ResponseID  string      `json:"responseId"`
    	QueryResult queryResult `json:"queryResult"`
    }
    
    // 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://godoc.org/google.golang.org/genproto/googleapis/cloud/dialogflow/v2#WebhookResponse
    type webhookResponse struct {
    	FulfillmentMessages []message `json:"fulfillmentMessages"`
    }
    
    // accountBalanceCheck handles the similar named action
    func accountBalanceCheck(ctx context.Context, request webhookRequest) (
    	webhookResponse, error) {
    	account := request.QueryResult.Parameters["account"].(string)
    	account = strings.ToLower(account)
    	var table string
    	if account == "savings account" {
    		table = "Savings"
    	} else {
    		table = "Checking"
    	}
    	s := "Your balance is $0"
    	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,
    			table,
    			spanner.Key{1}, // The account ID
    			[]string{"Balance"})
    		if err != nil {
    			if spanner.ErrCode(err) == codes.NotFound {
    				log.Printf("Account %d not found", 1)
    			} else {
    				return webhookResponse{}, err
    			}
    		} else {
    			// A row was returned, so check the value
    			var balance int64
    			err := row.Column(0, &balance)
    			if err != nil {
    				return webhookResponse{}, err
    			}
    			s = fmt.Sprintf("Your balance is $%.2f", float64(balance)/100.0)
    		}
    	}
    	response := webhookResponse{
    		FulfillmentMessages: []message{
    			{
    				Text: text{
    					Text: []string{s},
    				},
    			},
    		},
    	}
    	return response, nil
    }
    
    // Define a type for handler functions.
    type handlerFn func(ctx context.Context, request webhookRequest) (
    	webhookResponse, error)
    
    // Create a map from action to handler function.
    var handlers map[string]handlerFn = map[string]handlerFn{
    	"account.balance.check": accountBalanceCheck,
    }
    
    // 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 action from the request, and call the corresponding
    	// function that handles that action.
    	action := request.QueryResult.Action
    	if fn, ok := handlers[action]; ok {
    		response, err = fn(r.Context(), request)
    	} else {
    		err = fmt.Errorf("Unknown action: %s", action)
    	}
    	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
    	}
    }

  8. 按一下 [Deploy] (部署)

  9. 等待狀態指標顯示函式已成功部署。等待期間,請檢查剛部署的程式碼。

為服務專員設定 Webhook

由於 webhook 已成為服務,因此您必須將這個 webhook 與代理程式建立關聯。這項作業會透過執行作業完成。如要啟用及管理執行要求,請按照下列步驟操作:

  1. 前往 Dialogflow ES 主控台
  2. 選取剛剛建立的預先建構代理程式。
  3. 選取左側欄選單中的「Fulfillment」
  4. 將「Webhook」欄位切換為「Enabled」
  5. 提供您在上述步驟中複製的網址。將所有其他欄位留空。
  6. 按一下頁面底部的 [Save] (儲存)

啟用執行作業的螢幕截圖。

代理程式已啟用執行要求,因此您需要為意圖啟用執行要求:

  1. 選取左側欄選單中的 [Intents] (意圖)
  2. 選取 account.balance.check 意圖。
  3. 向下捲動至「Fulfillment」部分。
  4. 將「Enable webhook call for this intent」(為這個意圖啟用 Webhook 呼叫)切換為開啟。
  5. 按一下 [儲存]

試用服務專員

服務專員現已準備就緒,可以開始試用。按一下「Test Agent」按鈕,開啟模擬工具。請嘗試與代理人進行以下對話:

對話輪次 虛擬服務專員
1 您好 您好,感謝您選擇 ACME Bank。
2 我想知道我的帳戶餘額 你想查看哪個帳戶的餘額:儲蓄帳戶或支票帳戶?
3 檢查中 以下是你的最新餘額:$0.00

在第 3 次對話中,您提供的帳戶類型為「檢查」。account.balance.check 意圖包含名為 account 的參數。這個參數在此對話中設為「checking」。意圖的動作值也是「account.balance.check」。系統會呼叫 webhook 服務,並傳遞參數和動作值。

查看上述 webhook 程式碼時,您會發現這項動作會觸發類似名稱的函式呼叫。函式會判斷帳戶餘額。這個函式會檢查是否已設定特定環境變數,並提供連線至資料庫的資訊。如果未設定這些環境變數,函式會使用硬式編碼的帳戶餘額。在後續步驟中,您將變更函式的環境,讓函式從資料庫擷取資料。

疑難排解

Webhook 程式碼包含記錄陳述式。如果發生問題,請嘗試查看函式記錄。

更多資訊

如要進一步瞭解上述步驟,請參閱: