Criar um serviço de webhook

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

Criar a função

As funções do Cloud Run podem ser criadas com o console do Google Cloud (acesse a documentação, abra o console). Para criar uma função para este tutorial:

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

    Acessar o seletor de projetos

  2. Abra a página de visão geral das funções do Cloud Run.

    Acessar a visão geral das funções do Cloud Run

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

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

  5. Clique em Próxima (não é necessário usar 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 do Go mais recente.
    • Código-fonte: editor in-line
    • Ponto de entrada: HandleWebhookRequest
  7. Substitua o código por este:

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

  8. Clique em Implantar.

  9. Aguarde até que o indicador de status mostre que a função foi implantada. 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 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 criado pelo agente pré-criado.
  7. Atualize o URL do webhook para o URL que você salvou ao criar a função.
  8. Defina o Subtipo como "Padrão".
  9. Clique em Salvar.

Testar o agente

O agente já pode ser testado. Clique no botão Testar agente para abrir o simulador. Tente ter a seguinte conversa com o agente:

Turno de conversa Você Agente
1 Olá Olá! Sou o agente virtual de telecomunicações. Posso ajudar você a resolver dúvidas sobre faturamento e planejamento, solucionar problemas e adicionar planos de viagem e cruzeiros. Como posso ajudar você hoje?
2 Quero adicionar um plano de cruzeiro Vou verificar isso para você. Qual é seu número de telefone?
3 123-123-1234 Quantos dias dura o cruzeiro?
4 7 A opção 1 inclui ligações e mensagens. Ela custa $[XX] por dia. Mais informações aqui [URL]. A opção 2 inclui ligações, mensagens e dados. Ela custa $[XX] por dia. Mais informações aqui [URL]. Qual plano de cruzeiro você quer adicionar?
5 Opção 1 Entendi. Adicionei esse plano à sua conta. Durante o cruzeiro, você também pode precisar de cobertura em terra. Em qual porto você vai parar?
6 México E quantos dias você vai passar lá?
7 3 Temos uma ótima novidade! O México tem cobertura sem custo financeiro em terra. Tudo pronto. Posso ajudar com mais alguma coisa hoje?

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

No turno de conversa nº 6, você informou "México" como o porto de destino. O porto de destino e o número de dias são capturados como os parâmetros de formulário destination e trip_duration da página Collect Port. Navegue pelo agente para encontrar essas definições de parâmetros.

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 do formulário são fornecidos, essa rota é 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 está coberto pelo plano. A função verifica se variáveis de ambiente específicas estão definidas com informações para conexão ao banco de dados. Se essas variáveis de ambiente não estiverem definidas, a função usará uma lista codificada de destinos. Nas próximas etapas, você vai alterar o ambiente da função para que ela recupere dados de um banco de dados para validar a cobertura do plano para destinos.

Solução de problemas

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

Mais informações

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