Creare un servizio webhook

L'agente predefinito che hai creato nell'ultimo passaggio richiede un webhook. Le funzioni Cloud Run vengono utilizzate per ospitare l'webhook in questo tutorial per la loro semplicità, ma esistono molti altri modi per ospitare un servizio webhook. L'esempio utilizza anche il linguaggio di programmazione Go, ma puoi utilizzare qualsiasi supportato dalle funzioni Cloud Run.

Crea la funzione

Le funzioni Cloud Run possono essere create con la console Google Cloud (visita la documentazione, apri la console). Per creare una funzione per questo tutorial:

  1. È importante che l'agente Dialogflow e la funzione sono entrambi nello stesso progetto. Questo è il modo più semplice per Dialogflow di avere accesso sicuro alla tua funzione. Prima di creare la funzione, seleziona il tuo progetto dalla console Google Cloud.

    Vai al selettore progetti

  2. Apri la pagina di riepilogo delle funzioni di Cloud Run.

    Vai alla panoramica delle funzioni Cloud Run

  3. Fai clic su Crea funzione e imposta i seguenti campi:

    • Ambiente: 1ª generazione
    • Nome funzione: tutorial-telecommunications-webhook
    • Regione: se hai specificato una regione per l'agente, utilizza la stessa regione.
    • Tipo di trigger HTTP: HTTP
    • URL: fai clic sul pulsante di copia qui e salva il valore. Ti servirà questo URL durante la configurazione dell'webhook.
    • Autenticazione: richiedi l'autenticazione
    • Richiedi HTTPS: selezionato
  4. Fai clic su Salva.

  5. Fai clic su Avanti (non sono necessari runtime, build connessioni o impostazioni di sicurezza).

  6. Imposta i seguenti campi:

    • Runtime: seleziona la versione più recente del runtime Go.
    • Codice sorgente: editor in linea
    • Punto di ingresso: HandleWebhookRequest
  7. Sostituisci il codice con quanto segue:

    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. Fai clic su Esegui il deployment.

  9. Attendi finché l'indicatore di stato non indica che la funzione è stata dispiattata correttamente. Mentre aspetti, esamina il codice che hai appena disegnato.

Configura il webhook per l'agente

Ora che il webhook esiste come servizio, lo associerai al tuo agente. Per configurare il webhook per l'agente:

  1. Apri la console Dialogflow CX.
  2. Scegli il tuo progetto Google Cloud.
  3. Seleziona il tuo agente.
  4. Seleziona la scheda Gestisci.
  5. Fai clic su Webhook.
  6. Fai clic sul webhook esistente (creato dall'agente predefinito).
  7. Aggiorna l'URL webhook con l'URL webhook che hai salvato durante la creazione della funzione.
  8. Imposta Subtype (Sottotipo) su Standard.
  9. Fai clic su Salva.

Prova l'agente

L'agente è ora pronto per essere provato. Fai clic sul pulsante Test agente per aprire il simulatore. Prova ad avviare una conversazione con l'agente:

Turno di conversazione Tu Agente
1 Ciao Ciao! Sono l\'agente virtuale per le telecomunicazioni. Posso aiutarti a risolvere problemi di fatturazione e pianificazione, a risolvere i problemi e ad aggiungere piani di viaggio e crociera. Come posso aiutarti oggi?
2 voglio aggiungere un piano da crociera Fammi dare un'occhiata. Qual è il tuo numero di telefono registrato?
3 123-123-1234 Quanti giorni è la tua crociera?
4 7 L'opzione 1 include chiamate e SMS. Costa [XX] $ al giorno. Scopri di più qui [URL]. L'opzione 2 include chiamate, SMS e dati. Il costo è di $[XX] al giorno. Scopri di più qui [URL]. Quale piano di crociera vuoi aggiungere?
5 Opzione 1 Ok. Ho aggiunto il piano al tuo account. Durante la crociera, potresti anche aver bisogno di copertura a terra. In quale porto farai scalo?
6 Messico E per quanti giorni?
7 3 Ottime notizie. Il Messico ha copertura gratuita sulla terraferma. Ecco fatto. Posso fare qualcos'altro per aiutarti oggi?

Questa conversazione specifica è stata gestita dal flusso Cruise Plan.

Al sesto turno della conversazione, hai fornito "Messico" come porta di destinazione. La porta di destinazione e il numero di giorni vengono acquisiti come parametri del modulo destination e trip_duration della pagina Collect Port. Sfoglia il tuo agente per trovare queste definizioni di parametri.

Screenshot della pagina Raccogli porta nella console di Dialogflow

Nella pagina Collect Port, esiste un percorso con le condizioni per la compilazione del modulo: $page.params.status = "FINAL". Una volta forniti i due parametri del modulo, viene chiamato questo percorso. Questo percorso chiama il tuo webhook e fornisce il tag cruisePlanCoverage. Se esamini il codice dell'webhook riportato sopra, noti che questo tag attiva la chiamata della stessa funzione denominata.

Questa funzione determina se la destinazione fornita è coperto dal piano. La funzione controlla se sono impostate variabili di ambiente specifiche con informazioni per la connessione al database. Se queste variabili di ambiente non sono impostate, la funzione utilizza un elenco di destinazioni hardcoded. Nei passaggi successivi, alternerai l'ambiente per la funzione in modo che recuperi i dati da un database per convalidare la copertura del piano per le destinazioni.

Risoluzione dei problemi

Il codice webhook include istruzioni di logging. Se riscontri problemi, prova la visualizzazione dei log per la tua funzione.

Ulteriori informazioni

Per ulteriori informazioni sui passaggi precedenti, consulta: