您在上一步中创建的预构建代理需要一个 Webhook。由于 Cloud Run functions 简单易用,因此本教程使用它来托管 webhook,但您也可以通过许多其他方式来托管 webhook 服务。该示例还使用了 Go 编程语言,但您可以使用 Cloud Run functions 支持的任何语言。
创建函数
您可以使用 Google Cloud 控制台(访问文档,打开控制台)创建 Cloud Run functions。如需为本教程创建函数,请执行以下操作:
请务必确保 Dialogflow 代理和函数位于同一项目中。这是 Dialogflow 安全访问您的函数的最简单方法。在创建函数之前,请从 Google Cloud 控制台中选择您的项目。
打开 Cloud Run functions 概览页面。
点击创建函数,然后设置以下字段:
- 环境:第 1 代
- 函数名称:tutorial-telecommunications-webhook
- 区域:如果您为代理指定了区域,请使用同一区域。
- HTTP 触发器类型:HTTP
- 网址:点击此处的复制按钮,然后保存该值。 配置 webhook 时,您需要用到此网址。
- 身份验证:需要进行身份验证
- 需要 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]any `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]any{ "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]any{ "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]any{ "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]any{ "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]any{ "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 控制台。
- 选择您的 Google Cloud 项目。
- 选择您的代理。
- 选择管理标签页。
- 点击网络钩子。
- 点击现有 webhook(由预构建代理创建)。
- 将网络钩子网址更新为您在创建函数时保存的网络钩子网址。
- 将子类型设置为“标准”。
- 点击保存。
试用智能体
您的代理现在可以试用了。 点击测试代理按钮以打开模拟器。 尝试与智能体进行以下对话:
| 对话轮次 | 您 | 代理 |
|---|---|---|
| 1 | 您好! | 您好!我是虚拟电信客服。我可以帮您解决结算和方案问题、排查问题,以及添加旅行和游轮方案。您现在需要哪些方面的帮助? |
| 2 | 我想添加邮轮计划 | 让我帮您看看。您在我们这里登记的手机号码是什么? |
| 3 | 123-123-1234 | 您的邮轮行程有几天? |
| 4 | 7 | 方案 1 包含通话和短信。费用为 $[XX]/天。如需了解详情,请点击此处 [URL]。方案 2 包含通话、短信和流量。费用为每天 [XX] 美元。如需了解详情,请点击此处 [URL]。您想添加哪项邮轮计划? |
| 5 | 选项 1 | 知道了。我已将该方案添加到您的账号中。在游轮上,您可能还需要陆地上的网络覆盖。您将在哪个港口停靠? |
| 6 | 墨西哥 | 您要在那里待几天? |
| 7 | 3 | 好消息!墨西哥提供免费的陆上覆盖。大功告成!您现在还需要其他方面的帮助吗? |
此特定对话由 Cruise Plan 流程管理。
在第 6 轮对话中,您提供了“墨西哥”作为目的港。
目的地港口和停留天数会作为 Collect Port 页面的 destination 和 trip_duration 表单参数进行捕获。浏览您的代理,找到这些参数定义。

在 Collect Port 页面中,有一个用于表单完成的条件路由:$page.params.status = "FINAL"。提供这两个表单参数后,系统会调用此路由。
此路由会调用您的网络钩子,并向其提供 cruisePlanCoverage 标记。如果您查看上面的 webhook 代码,会发现此标记会触发调用同名函数。
此函数用于确定提供的目的地是否在方案覆盖范围内。 该函数会检查是否设置了特定环境变量,其中包含用于连接到数据库的信息。如果未设置这些环境变量,该函数将使用硬编码的目标位置列表。在接下来的步骤中,您将更改函数的环境,使其从数据库中检索数据,以验证目的地是否在套餐覆盖范围内。
问题排查
Webhook 代码包含日志语句。如果您遇到问题,请尝试查看函数的日志。
更多信息
如需详细了解上述步骤,请参阅: