实现 Webhook

本指南提供了各种用于实现 网络钩子 的示例,以及网络钩子问题排查建议。

设置会话参数

以下示例展示了如何设置会话参数。

Go

如需向 Dialogflow CX 进行身份验证,请设置应用默认凭据。 如需了解详情,请参阅 为本地开发环境设置身份验证

请参阅 网络钩子快速入门

Java

如需向 Dialogflow CX 进行身份验证,请设置应用默认凭据。 如需了解详情,请参阅 为本地开发环境设置身份验证


// TODO: Change class name to Example
// TODO: Uncomment the line below before running cloud function
// package com.example;

import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import java.io.BufferedWriter;

public class WebhookConfigureSessionParameters implements HttpFunction {
  @Override
  public void service(HttpRequest request, HttpResponse response) throws Exception {
    JsonObject orderParameter = new JsonObject();
    orderParameter.addProperty("order_number", "12345");

    JsonObject parameterObject = new JsonObject();
    parameterObject.add("parameters", orderParameter);

    // Creates webhook response object
    JsonObject webhookResponse = new JsonObject();
    webhookResponse.add("session_info", parameterObject);

    Gson gson = new GsonBuilder().setPrettyPrinting().create();
    String jsonResponseObject = gson.toJson(webhookResponse);

    /** { "session_info": { "parameters": { "order_number": "12345" } } } */
    BufferedWriter writer = response.getWriter();
    // Sends the webhookResponseObject
    writer.write(jsonResponseObject.toString());
  }
}

Node.js

如需向 Dialogflow CX 进行身份验证,请设置应用默认凭据。 如需了解详情,请参阅 为本地开发环境设置身份验证

const functions = require('@google-cloud/functions-framework');

functions.http('configureSessionParams', (request, response) => {
  // Session parameter configured by the webhook
  const orderNumber = 123;

  const jsonResponse = {
    sessionInfo: {
      parameters: {
        orderNumber: orderNumber,
      },
    },
  };

  response.send(jsonResponse);
});

Python

如需向 Dialogflow CX 进行身份验证,请设置应用默认凭据。 如需了解详情,请参阅 为本地开发环境设置身份验证

import functions_framework

# TODO (developer): change entry point to configure_session_params in Cloud Function


@functions_framework.http
def configure_session_params(request):
    """Webhook to validate or configure new session parameters."""

    order_number = 123

    json_response = {
        "sessionInfo": {
            "parameters": {
                "orderNumber": order_number,
            },
        },
    }

    return json_response

返回 fulfillment 响应

以下示例展示了如何返回 fulfillment 响应。

Go

如需向 Dialogflow CX 进行身份验证,请设置应用默认凭据。 如需了解详情,请参阅 为本地开发环境设置身份验证

请参阅 网络钩子快速入门

Java

如需向 Dialogflow CX 进行身份验证,请设置应用默认凭据。 如需了解详情,请参阅 为本地开发环境设置身份验证


// TODO: Change class name to Example
// TODO: add GSON dependency to Pom file
// (https://mvnrepository.com/artifact/com.google.code.gson/gson/2.8.5)
// TODO: Uncomment the line bellow before running cloud function
// package com.example;

import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.io.BufferedWriter;

public class BasicWebhook implements HttpFunction {
  @Override
  public void service(HttpRequest request, HttpResponse response) throws Exception {
    Gson gson = new GsonBuilder().create();
    JsonObject parsedRequest = gson.fromJson(request.getReader(), JsonObject.class);

    // For more information on the structure of this object https://cloud.google.com/dialogflow/cx/docs/reference/rest/v3/Fulfillment
    String requestTag = parsedRequest.getAsJsonObject("fulfillmentInfo")
        .getAsJsonPrimitive("tag").toString();
    JsonObject responseObject = null;
    String defaultIntent = "\"Default Welcome Intent\"";
    String secondIntent = "\"get-agent-name\"";
    String responseText = "";

    // Compares the Intent Tag to provide the correct response 
    if (requestTag.equals(defaultIntent)) {
      responseText = "\"Hello from a Java GCF Webhook\"";
    } else if (requestTag.equals(secondIntent)) {
      responseText = "\"My name is Flowhook\"";
    } else {
      responseText = "\"Sorry I didn't get that\"";
    }

    // Constructing the response jsonObject 
    responseObject =
        JsonParser
            .parseString(
                "{ \"fulfillment_response\": { \"messages\": [ { \"text\": { \"text\": ["
                    + responseText
                    + "] } } ] } }")
            .getAsJsonObject();
    BufferedWriter writer = response.getWriter();

    //Sends the responseObject
    writer.write(responseObject.toString());
  }
}

Node.js

如需向 Dialogflow CX 进行身份验证,请设置应用默认凭据。 如需了解详情,请参阅 为本地开发环境设置身份验证

const functions = require('@google-cloud/functions-framework');

functions.http('handleWebhook', (request, response) => {
  const tag = request.body.fulfillmentInfo.tag;
  let text = '';

  if (tag === 'Default Welcome Intent') {
    text = 'Hello from a GCF Webhook';
  } else if (tag === 'get-name') {
    text = 'My name is Flowhook';
  } else {
    text = `There are no fulfillment responses defined for "${tag}"" tag`;
  }

  const jsonResponse = {
    fulfillment_response: {
      messages: [
        {
          text: {
            //fulfillment text response to be sent to the agent
            text: [text],
          },
        },
      ],
    },
  };

  response.send(jsonResponse);
});

Python

如需向 Dialogflow CX 进行身份验证,请设置应用默认凭据。 如需了解详情,请参阅 为本地开发环境设置身份验证

import functions_framework

# TODO(developer): change entry point to handle_webhook in cloud function


@functions_framework.http
def handle_webhook(request):
    req = request.get_json()

    tag = req["fulfillmentInfo"]["tag"]

    if tag == "Default Welcome Intent":
        text = "Hello from a GCF Webhook"
    elif tag == "get-name":
        text = "My name is Flowhook"
    else:
        text = f"There are no fulfillment responses defined for {tag} tag"

    # You can also use the google.cloud.dialogflowcx_v3.types.WebhookRequest protos instead of manually writing the json object
    # Please see https://googleapis.dev/python/dialogflow/latest/dialogflow_v2/types.html?highlight=webhookresponse#google.cloud.dialogflow_v2.types.WebhookResponse for an overview
    res = {"fulfillment_response": {"messages": [{"text": {"text": [text]}}]}}

    # Returns json
    return res

根据需要设置表单参数

以下示例展示了如何将参数标记为必需。

Java

如需向 Dialogflow CX 进行身份验证,请设置应用默认凭据。 如需了解详情,请参阅 为本地开发环境设置身份验证


// TODO: Change class name to Example
// TODO: Uncomment the line below before running cloud function
// package com.example;

import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import java.io.BufferedWriter;

public class ConfigureWebhookToSetFormParametersAsOptionalOrRequired implements HttpFunction {
  @Override
  public void service(HttpRequest request, HttpResponse response) throws Exception {
    JsonObject parameterObject = new JsonObject();
    parameterObject.addProperty("display_name", "order_number");
    parameterObject.addProperty("required", "true");
    parameterObject.addProperty("state", "VALID");

    JsonArray parameterInfoList = new JsonArray();
    parameterInfoList.add(parameterObject);

    JsonObject parameterInfoObject = new JsonObject();
    parameterInfoObject.add("parameter_info", parameterInfoList);

    JsonObject formInfo = new JsonObject();
    formInfo.add("form_info", parameterInfoObject);

    // Constructs the webhook response object
    JsonObject webhookResponse = new JsonObject();
    webhookResponse.add("page_info", formInfo);

    Gson gson = new GsonBuilder().setPrettyPrinting().create();
    String jsonResponseObject = gson.toJson(webhookResponse);

    /* {
     *   "page_info": {
     *     "form_info": {
     *       "parameter_info": [
     *         {
     *           "display_name": "order_number",
     *           "required": "true",
     *           "state": "VALID"
     *         }
     *       ]
     *     }
     *   }
     * }
     */

    BufferedWriter writer = response.getWriter();

    // Sends the responseObject
    writer.write(jsonResponseObject.toString());
  }
}

Node.js

如需向 Dialogflow CX 进行身份验证,请设置应用默认凭据。 如需了解详情,请参阅 为本地开发环境设置身份验证

const functions = require('@google-cloud/functions-framework');

functions.http('configureOptionalFormParam', (request, response) => {
  // The value of the parameter that the webhook will set as optional or required.
  // Note that the webhook cannot add or remove any form parameter

  const jsonResponse = {
    pageInfo: {
      formInfo: {
        parameterInfo: [
          {
            displayName: 'order-number',
            // if required: false, the agent will not reprompt for this parameter, even if the state is 'INVALID'
            required: true,
            state: 'VALID',
          },
        ],
      },
    },
  };

  // Info about form parameter that is sent in the webhook response:
  console.log(
    'Parameter Info: \n',
    jsonResponse.pageInfo.formInfo.parameterInfo[0]
  );

  response.send(jsonResponse);
});

验证表单参数

以下示例展示了如何验证表单参数。

Java

如需向 Dialogflow CX 进行身份验证,请设置应用默认凭据。 如需了解详情,请参阅 为本地开发环境设置身份验证


// TODO: Change class name to Example
// TODO: Uncomment the line below before running cloud function
// package com.example;

import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import java.io.BufferedWriter;

public class WebhookValidateFormParameter implements HttpFunction {
  @Override
  public void service(HttpRequest request, HttpResponse response) throws Exception {
    JsonObject sessionInfo = new JsonObject();
    JsonObject sessionParameter = new JsonObject();

    sessionParameter.addProperty("order_number", "null");
    sessionInfo.add("parameters", sessionParameter);

    JsonObject parameterObject = new JsonObject();
    parameterObject.addProperty("display_name", "order_number");
    parameterObject.addProperty("required", "true");
    parameterObject.addProperty("state", "INVALID");
    parameterObject.addProperty("value", "123");

    JsonArray parameterInfoList = new JsonArray();
    parameterInfoList.add(parameterObject);

    JsonObject parameterInfoObject = new JsonObject();
    parameterInfoObject.add("parameter_info", parameterInfoList);

    JsonObject pageInfo = new JsonObject();
    pageInfo.add("form_info", parameterInfoObject);

    // Constructs the webhook response object
    JsonObject webhookResponse = new JsonObject();
    webhookResponse.add("page_info", pageInfo);
    webhookResponse.add("session_info", sessionInfo);

    Gson gson = new GsonBuilder().setPrettyPrinting().create();
    String jsonResponseObject = gson.toJson(webhookResponse);

    /**
     * { "page_info": { "form_info": { "parameter_info": [ { "display_name": "order_number",
     * "required": "true", "state": "INVALID", "value": "123" } ] } }, "session_info": {
     * "parameters": { "order_number": "null" } } }
     */
    BufferedWriter writer = response.getWriter();

    // Sends the responseObject
    writer.write(jsonResponseObject.toString());
  }
}

Node.js

如需向 Dialogflow CX 进行身份验证,请设置应用默认凭据。 如需了解详情,请参阅 为本地开发环境设置身份验证

const functions = require('@google-cloud/functions-framework');

functions.http('validateParameter', (request, response) => {
  // Webhook will validate or invalidate parameter based on logic configured by the user.
  // Access parameter values through the webhook request via `request.body.pageInfo.formInfo.parameterInfo[]`
  const jsonResponse = {
    page_info: {
      form_info: {
        parameter_info: [
          {
            displayName: 'orderNumber',
            required: true,
            state: 'INVALID',
            value: 123,
          },
        ],
      },
    },
    sessionInfo: {
      parameters: {
        // Set session parameter to null if the form parameter is 'INVALID' and your agent needs to reprompt the user
        orderNumber: null,
      },
    },
  };

  response.send(jsonResponse);
});

Python

如需向 Dialogflow CX 进行身份验证,请设置应用默认凭据。 如需了解详情,请参阅 为本地开发环境设置身份验证

import functions_framework


@functions_framework.http
def validate_parameter(request):
    """Webhook will validate or invalidate parameter based on logic configured by the user."""
    return {
        "page_info": {
            "form_info": {
                "parameter_info": [
                    {
                        "displayName": "orderNumber",
                        "required": True,
                        "state": "INVALID",
                        "value": 123,
                    },
                ],
            },
        },
        "sessionInfo": {
            "parameters": {
                # Set session parameter to None if the form parameter is 'INVALID' and your agent needs to reprompt the user
                "orderNumber": None,
            },
        },
    }

记录会话 ID

以下示例展示了如何记录网络钩子请求中的 session ID

Python

如需向 Dialogflow CX 进行身份验证,请设置应用默认凭据。 如需了解详情,请参阅 为本地开发环境设置身份验证


import re

import functions_framework


@functions_framework.http
def log_session_id_for_troubleshooting(request):
    """Webhook will log session id corresponding to request."""

    req = request.get_json()
    # You can read more about SessionInfo at https://cloud.google.com/dialogflow/cx/docs/reference/rest/v3/SessionInfo
    # Use a regex pattern to get the session ID
    session_id_regex = r".+\/sessions\/(.+)"
    session = req["sessionInfo"]["session"]
    regex_match = re.search(session_id_regex, session)
    session_id = regex_match.group(1)

    # Instead of printing, use the logging tools available to you
    print(f"Debug Node: session ID = {session_id}")

    # Return a generic response
    res = {
        "fulfillment_response": {
            "messages": [{"text": {"text": [f"Request Session ID: {session_id}"]}}]
        }
    }

    # Returns json
    return res

问题排查

网络钩子调用的生命周期

网络钩子调用始终由 Dialogflow CX 发起,并通过 HTTPS 发送到 Web 服务器。通用 Web 服务网络钩子调用源自 Google 拥有的互联网 IP 地址,并且可以访问公共互联网中可用的网络服务器(网络钩子服务器)。 另一方面,Service Directory 网络钩子始终从内部 Google Cloud 地址开始,并且只能访问专用网络 中的网络钩子服务器 Google Cloud。

用于调试网络钩子的实用日志

调试网络钩子问题通常涉及收集 Cloud Logging Dialogflow CX 日志和网络钩子服务器日志。如果网络钩子服务器是使用 Cloud Run functions 实现的,则其日志将位于 Cloud Logging 中。否则,日志通常会位于网络钩子服务器运行的位置。

标准网络钩子日志包含一个带有 UUID 的 detectIntentResponseId 字段,该字段可用于跟踪网络钩子服务器中的特定调用。启用 Cloud Logging 后,此日志将存在于 Dialogflow CX Cloud Logging 日志中。

常见的网络钩子问题

您可以在 Dialogflow CX 日志中找到一些网络钩子调用错误,如下所示:

网络钩子服务器主机名解析错误

Dialogflow CX 查找了通用网络钩子的主机名,但该主机名在 DNS 中不存在。请确保主机名已在公共 DNS 中注册。如果主机名是新的,则记录可能需要一段时间才能传播。Cloud Logging 消息:State: URL_ERROR, Reason: ERROR_DNS

网络钩子服务器返回客户端错误

除了 ERROR_DNS 之外,此状态还表示网络钩子服务器返回了 4xx 响应。这可能是未经授权的状态 (401 - ERROR_AUTHENTICATION),也可能是网络钩子服务器中找不到网址 (404 - ERROR_NOT_FOUND)。Cloud Logging 消息:State: URL_ERROR

Dialogflow 代理在网络钩子服务器返回响应之前超时

Dialogflow CX 在 Web 服务器完成之前达到了网络钩子超时限制。这里有两种可能的方法:缩短网络钩子服务器处理时间,或增加 Dialogflow CX 等待网络钩子的时间。虽然在许多情况下缩短处理时间并非易事,但通常会带来最佳结果。请注意,网络钩子存在最长超时限制,并且最终调用者或用户需要等待更长时间才能从代理处获得答案,然后再增加此设置。Cloud Logging 消息:State: URL_TIMEOUT, Reason: TIMEOUT_WEB

gRPC 在网络钩子服务器返回响应之前超时

在网络钩子调用完成之前,Dialogflow CX API 调用中 gRPC 设置的时间限制已达到。此限制通常在集成级别设置,并且与 Dialogflow CX 参数和网络钩子超时限制无关。如需详细了解 gRPC 截止期限,请参阅 https://grpc.io/docs/guides/deadlines/。 Cloud Logging 消息:State: URL_REJECTED, Reason: REJECTED_DEADLINE_EXCEEDED

Dialogflow 无法与网络钩子服务器联系

由于网络错误,无法访问网络钩子服务器;或者连接已建立,但网络钩子服务器返回了 HTTP 状态 5xx,表明在处理请求时出现问题。确保 Dialogflow CX 可以在网络级别访问网络钩子服务器地址。如果请求显示在网络钩子服务器日志中,请找出调用返回 5xx 错误的原因。Cloud Logging 消息: State: URL_UNREACHABLE

跟踪网络钩子调用

可以使用会话 ID、detectIntentResponse ID、Cloud Run functions 的跟踪 ID 和调用时间戳,在 Dialogflow CX 和网络钩子服务器之间关联标准网络钩子调用。可以使用调用时间戳和设计时在网络钩子定义中指定的会话参数值来完成灵活的网络钩子跟踪。如需详细了解标准和灵活的网络钩子请求,请参阅 网络钩子

会话 ID 显示在 sessionInfo.session 字段的 WebhookRequest中。 此会话 ID 对于每个对话都应该是唯一的,并且可以帮助您将代理日志与使用相同会话 ID 的请求的网络钩子日志进行比较。上一部分 记录会话 ID 部分 展示了如何从网络钩子记录会话 ID。

此外,如果您在 Cloud Run functions 或类似的 Google Cloud 无服务器选项上托管网络钩子, 则可以使用trace字段作为 日志条目 的日志过滤条件。 一次执行函数会导致多个日志条目具有相同的跟踪值。

下一个示例同时使用会话 ID 和跟踪值,将特定的 Dialogflow CX 代理错误日志与相应的 Cloud Run functions 网络钩子日志条目相关联。 该示例对已启用 Cloud Logging的代理使用 Cloud Logging 过滤条件

1. 过滤 Dialogflow CX 日志以获取特定代理的错误日志

使用以下 Cloud Logging 过滤条件过滤 Dialogflow CX 日志以获取特定代理的错误日志:

labels.location_id="global"
labels.agent_id="AGENT_ID"
severity=ERROR

网络钩子日志错误条目如下所示:

{
  "insertId": "-j4gkkre31e2o",
  "jsonPayload": {
    "code": 14,
    "message": "Error calling webhook 'https://us-central1-PROJECT_ID.cloudfunctions.net/function-webhook': State: URL_UNREACHABLE, Reason: UNREACHABLE_5xx, HTTP status code: 500"
  },
  "labels": {
    "agent_id": "e9e01392-1351-42dc-9b15-b583fb2d2881",
    "environment_id": "",
    "location_id": "global",
    "session_id": "07c899-a86-78b-a77-569625b37"
  },
  "logName": "projects/PROJECT_ID/logs/dialogflow-runtime.googleapis.com%2Frequests",
  "receiveTimestamp": "2024-10-28T21:49:04.288439054Z",
  "resource": {
    "labels": {
      "project_id": "PROJECT_ID"
    },
    "type": "global",
  },
  "severity": "ERROR",
  "timestamp": "2024-10-28T21:49:04.132548Z"
}

请注意包含会话 ID 的 labels.session_id 字段。 您将在下一步中使用会话 ID。

2. 按会话 ID 过滤 Cloud Run functions 日志

使用以下 Cloud Logging 过滤条件按会话 ID 过滤 Cloud Run functions 日志:

resource.type = "cloud_run_revision"
resource.labels.service_name = "CLOUD_RUN_FUNCTION_NAME"
resource.labels.location = "CLOUD_RUN_FUNCTION_REGION"
textPayload="Debug Node: session ID = SESSION_ID"

生成的日志对应于在提供的 会话期间生成的网络钩子日志。 例如:

{
  "insertId": "671c42940007ebebdbb1d56e",
  "labels": {
    "execution_id": "pgy8jvvblovs",
    "goog-managed-by": "cloudfunctions",
    "instance_id": "004940b3b8e3d975a4b11a4ed7d1ded4ce3ed37467ffc5e2a8f13a1908db928f8200b01cc554a5eda66ffc9d23d76dd75cec1619a07cb5751fa2e8a93bc6cfc3df86dfa0650a"
  },
  "logName": "projects/PROJECT_ID/logs/run.googleapis.com%2Fstdout",
  "receiveTimestamp": "2024-10-26T01:15:00.523313187Z",
  "resource": {
    "labels": {
      "configuration_name": "function-webhook",
      "location": "us-central1",
      "project_id": "PROJECT_ID",
      "revision_name": "function-webhook-00001-jiv",
      "service_name": "function-webhook",
    },
    "type": "cloud_run_revision"
  },
  "spanId": "6938366936362981595",
  "trace": "d1b54fbc8945dd59bdcaed37d7d5e185",
  "textPayload": "Debug Node: session ID = 07c899-a86-78b-a77-569625b37",
  "timestamp": "2024-10-26T01:15:00.519147Z"
}

请注意 trace 字段,该字段将在下一步中使用。

3. 过滤 Cloud Function 日志以获取特定跟踪

使用以下 Cloud Logging 过滤条件过滤 Cloud Function 日志以获取特定跟踪:

resource.type = "cloud_run_revision"
resource.labels.service_name = "CLOUD_RUN_FUNCTION_NAME"
resource.labels.location = "CLOUD_RUN_FUNCTION_REGION"
trace="projects/PROJECT_ID/traces/TRACE_ID"

其中,TRACE_ID 是跟踪的最后一个片段。例如,TRACE_IDprojects/PROJECT_ID/traces/e41eefc1fac48665b442bfa400cc2f5ee41eefc1fac48665b442bfa400cc2f5e

结果是执行与第 1 步中的会话 ID 和第 2 步中的跟踪相关联的网络钩子请求期间生成的网络钩子服务器日志。日志如下所示。

{
  "insertId": "671c42940008465e29f5faf0",
  "httpRequest": {
    "requestMethod": "POST",
    "requestUrl": "https://us-central1-TEST_PROJECT.cloudfunctions.net/function-webhook",
    "requestSize": "2410",
    "status": 200,
    "responseSize": "263",
    "userAgent": "Google-Dialogflow",
    "remoteIp": "8.34.210.1",
    "serverIp": "216.239.36.1",
    "latency": "0.166482342s",
    "protocol": "HTTP/1.1"
  },
  "resource": {
    "type": "cloud_run_revision",
    "labels": {
      "project_id": "PROJECT_ID",
      "service_name": "function-webhook",
      "location": "us-central1",
      "revision_name": "function-webhook-00001-jiv",
      "configuration_name": "function-webhook"
    }
  },
  "timestamp": "2024-10-26T01:15:00.352197Z",
  "severity": "INFO",
  "labels": {
    "instanceId": "004940b3b813af8a656c92aac1bd07ffad5165f1353e1e346b6161c14bcde225f68f4a88ceedc08aa9020f387b1b59471f73de45f2882a710ced37dea921f05ad962347690be",
    "goog-managed-by": "cloudfunctions"
  },
  "logName": "projects/test-project-12837/logs/run.googleapis.com%2Frequests",
  "trace": "projects/test-project-12837/traces/d1b54fbc8945dd59bdcaed37d7d5e185",
  "receiveTimestamp": "2024-10-26T01:15:00.548931586Z",
  "spanId": "604a07f7b33b18db",
  "traceSampled": true
}