创建 webhook 服务

您在上一步中创建的预构建代理无法提供账号余额等动态数据,因为所有内容都已硬编码到代理中。在本教程的这一步中,您将创建一个可向代理提供动态数据的webhook。本教程中,我们使用 Cloud Run 函数托管 webhook,因为它们简单易用,但您还可以通过许多其他方式托管 webhook 服务。该示例还使用了 Go 编程语言,但您可以使用 Cloud Run functions 支持的任何语言

创建函数

您可以使用 Google Cloud 控制台创建 Cloud Run 函数(访问文档打开控制台)。如需为本教程创建函数,请执行以下操作:

  1. 请务必将 Dialogflow 代理和函数放在同一项目中。这是 Dialogflow 安全访问您的函数的最简单方法。在创建函数之前,请在 Google Cloud 控制台中选择您的项目。

    转到“项目选择器”

  2. 打开 Cloud Run functions 概览页面。

    前往 Cloud Run 函数概览

  3. 点击创建函数,然后设置以下字段:

    • 环境:第 1 代
    • 函数名称:tutorial-banking-webhook
    • 区域:如果您为代理指定了区域,请使用相同的区域。
    • HTTP 触发器类型:HTTP
    • 网址:点击此处的复制按钮,然后保存该值。 您在配置该网络钩子时需要使用此网址。
    • 身份验证:需要进行身份验证
    • 需要 HTTPS:已选中
  4. 点击保存

  5. 点击下一步(您无需特殊的运行时、构建、连接或安全设置)。

  6. 设置以下字段:

    • 运行时:选择最新的 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. 点击部署

  9. 等待状态指示器显示函数已成功部署。在等待期间,检查您刚刚部署的代码。

为您的代理配置网络钩子

现在,网络钩子已作为服务存在,您需要将此网络钩子与您的代理相关联。这是通过执行方式完成的。如需为您的代理启用和管理 fulfillment,请执行以下操作:

  1. 前往 Dialogflow ES 控制台
  2. 选择您刚刚创建的预构建代理。
  3. 选择左侧边栏菜单中的 Fulfillment
  4. 网路钩子字段切换为已启用
  5. 提供您从上方复制的网址。 将其他所有字段留空。
  6. 点击页面底部的保存

启用 fulfillment 的屏幕截图。

现在,为代理启用了 fulfillment,您需要为 intent 启用 fulfillment:

  1. 在左侧边栏菜单中选择意图 (Intents)。
  2. 选择 account.balance.check intent。
  3. 向下滚动到 Fulfillment 部分。
  4. 开启为此意图启用 webhook 调用
  5. 点击保存

试用代理

您的客服人员现在可以开始试用了。 点击测试代理按钮以打开模拟器。尝试与客服人员进行以下对话:

对话回合 代理
1 您好! 您好!感谢您选择 ACME 银行。
2 我想知道我的账号余额 您希望将余额存入哪个账户:储蓄账户还是支票账户?
3 正在检查 您的最新余额为:0.00 美元

在第 3 轮对话中,您提供了“支票账户”作为账号类型。account.balance.check intent 有一个名为 account 的参数。在此对话中,此参数设置为“checking”。 该 intent 的操作值也为“account.balance.check”。系统会调用网络钩子服务,并将参数和操作值传递给该服务。

如果您查看上面的 webhook 代码,就会发现此操作会触发调用同名函数。该函数用于确定账号余额。该函数会检查是否已使用连接到数据库的信息设置特定环境变量。如果未设置这些环境变量,该函数将使用硬编码的账号余额。在后续步骤中,您将更改函数的环境,以便它从数据库中检索数据。

问题排查

该 webhook 代码包含日志记录语句。如果您遇到问题,请尝试查看函数的日志

更多信息

如需详细了解上述步骤,请参阅: