Criar um serviço de webhook

O agente pré-criado que você criou na última etapa requer um webhook. As funções do Cloud são usadas para hospedar o webhook neste tutorial devido à simplicidade dele, mas há muitas outras maneiras de hospedar um serviço de webhook. No exemplo, também usamos a linguagem de programação Go, mas é possível usar qualquer linguagem compatível com o Cloud Functions.

Criar a função

O Cloud Functions pode ser criado com o console do Google Cloud. Acesse a documentação e abra o console. Para criar uma função para este tutorial:

  1. É importante que o agente e a função do Dialogflow estejam no mesmo projeto. Essa é a maneira mais fácil de o Dialogflow ter acesso seguro à função. Antes de criar a função, selecione seu projeto no console do Google Cloud.

    Acessar o seletor de projetos

  2. Abra a página de visão geral do Cloud Functions.

    Acessar a visão geral do Cloud Functions

  3. Clique em Criar função e defina os seguintes campos:

    • Ambiente: 1a geração
    • Nome da função: tutorial-telecommunications-webhook
    • Região: se você especificou uma região para seu agente, use a mesma região.
    • Tipo de acionador HTTP: HTTP
    • URL: clique no botão de cópia aqui e salve o valor. Você precisará desse URL ao configurar o webhook.
    • Autenticação: exige autenticação.
    • Exigir HTTPS: marcada
  4. Clique em Save.

  5. Clique em Next. Você não precisa de configurações especiais de ambiente de execução, build, conexões ou segurança.

  6. Defina os seguintes campos:

    • Ambiente de execução: selecione o ambiente de execução mais recente do Go.
    • Código-fonte: editor in-line
    • Ponto de entrada: HandleWebhookRequest
  7. Substitua o código pelo seguinte:

    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
    	}
    }
    
    

  8. Clique em Implantar.

  9. Aguarde até que o indicador de status mostre que a função foi implantada com sucesso. Enquanto espera, examine o código que você acabou de implantar.

Configurar o webhook do agente

Agora que o webhook existe como um serviço, você vai associá-lo ao seu agente. Para configurar o webhook do agente:

  1. Abra o Console do Dialogflow CX.
  2. Escolha seu projeto do Google Cloud.
  3. Selecione seu agente.
  4. Selecione a guia Gerenciar.
  5. Clique em Webhooks.
  6. Clique no webhook que você criou, criado pelo agente pré-criado.
  7. Atualize o URL do webhook como o URL que você salvou ao criar a função.
  8. Defina o Subtipo como Padrão.
  9. Clique em Save.

Testar o agente

Seu agente está pronto para tentar. Clique no botão Testar agente para abrir o simulador. Tente ter a seguinte conversa com o agente:

Turno da conversa Você Agente
1 Olá Olá! Sou o agente de telecomunicações virtual. Posso ajudar você a resolver dúvidas sobre faturamento e planejamento, resolver problemas e adicionar planos de viagem e cruzeiro. Como posso ajudar você hoje?
2 Quero adicionar um plano de cruzeiro Vou dar uma olhada para você. Qual é seu número de telefone conosco?
3 123-123-1234 Quantos dias dura o cruzeiro?
4 7 A opção 1 inclui fala e texto. custa US $[XX]/dia. Mais informações aqui [URL]. A opção 2 inclui fala, texto e dados. custa US $[XX]/dia. Mais informações aqui [URL]. Qual plano de cruzeiro você quer adicionar?
5 Opção 1 Ok. Adicionei esse plano à sua conta. Durante o cruzeiro, talvez você também precise ter cobertura em terra. Em que porta você fará uma parada?
6 México E quantos dias você passa nele?
7 3 Ótima notícia! O México tem cobertura terrestre gratuita. Tudo pronto. Posso ajudar com mais alguma coisa hoje?

Esta conversa específica foi gerenciada pelo fluxo Cruise Plan.

Na rodada 6, você forneceu "México" como sua porta de destino. A porta de destino e o número de dias nela são capturados como parâmetros de formulário destination e trip_duration da página Collect Port. Procure essas definições de parâmetros no seu agente.

Captura de tela da página "Coletar porta" no console do Dialogflow

Na página Collect Port, há uma rota de condição para o preenchimento do formulário: $page.params.status = "FINAL". Depois que os dois parâmetros de formulário forem fornecidos, essa rota será chamada. Essa rota chama o webhook e fornece a tag cruisePlanCoverage ao webhook. Se você examinar o código do webhook acima, verá que essa tag aciona a mesma função nomeada a ser chamada.

Essa função determina se o destino fornecido é coberto pelo plano. A função verifica se variáveis de ambiente específicas estão definidas com informações para se conectar ao banco de dados. Se essas variáveis de ambiente não estiverem definidas, a função usará uma lista de destinos fixada no código. Nas próximas etapas, você vai alterar o ambiente da função para que ela recupere dados de um banco de dados e valide a cobertura do plano para destinos.

Solução de problemas

O código do webhook inclui instruções de geração de registros. Se você tiver problemas, tente visualizar os registros da sua função.

Mais informações

Para mais informações sobre as etapas acima, consulte: