The prebuilt agent you created in the last step cannot provide dynamic data like account balances, because everything is hardcoded into the agent. In this step of the tutorial, you will create a webhook that can provide dynamic data to the agent. Cloud Run functions are used to host the webhook in this tutorial due to their simplicity, but there are many other ways that you could host a webhook service. The example also uses the Go programming language, but you can use any language supported by Cloud Run functions.
Create the Function
Cloud Run functions can be created with the Google Cloud console (visit documentation, open console). To create a function for this tutorial:
It is important that your Dialogflow agent and the function are both in the same project. This is the easiest way for Dialogflow to have secure access to your function. Before creating the function, select your project from the Google Cloud console.
Open the Cloud Run functions overview page.
Click Create Function, and set the following fields:
- Environment: 1st gen
- Function name: tutorial-banking-webhook
- Region: If you specified a region for your agent, use the same region.
- HTTP Trigger type: HTTP
- URL: Click the copy button here and save the value. You will need this URL when configuring the webhook.
- Authentication: Require authentication
- Require HTTPS: checked
Click Save.
Click Next (You do not need special runtime, build, connections, or security settings).
Set the following fields:
- Runtime: Select the latest Go runtime.
- Source code: Inline Editor
- Entry point: HandleWebhookRequest
Replace the code with the following:
package estwh 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 queryResult struct { Action string `json:"action"` Parameters map[string]interface{} `json:"parameters"` } type text struct { Text []string `json:"text"` } type message struct { Text text `json:"text"` } // 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://godoc.org/google.golang.org/genproto/googleapis/cloud/dialogflow/v2#WebhookRequest type webhookRequest struct { Session string `json:"session"` ResponseID string `json:"responseId"` QueryResult queryResult `json:"queryResult"` } // 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://godoc.org/google.golang.org/genproto/googleapis/cloud/dialogflow/v2#WebhookResponse type webhookResponse struct { FulfillmentMessages []message `json:"fulfillmentMessages"` } // accountBalanceCheck handles the similar named action func accountBalanceCheck(ctx context.Context, request webhookRequest) ( webhookResponse, error) { account := request.QueryResult.Parameters["account"].(string) account = strings.ToLower(account) var table string if account == "savings account" { table = "Savings" } else { table = "Checking" } s := "Your balance is $0" 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, table, spanner.Key{1}, // The account ID []string{"Balance"}) if err != nil { if spanner.ErrCode(err) == codes.NotFound { log.Printf("Account %d not found", 1) } else { return webhookResponse{}, err } } else { // A row was returned, so check the value var balance int64 err := row.Column(0, &balance) if err != nil { return webhookResponse{}, err } s = fmt.Sprintf("Your balance is $%.2f", float64(balance)/100.0) } } response := webhookResponse{ FulfillmentMessages: []message{ { Text: text{ Text: []string{s}, }, }, }, } return response, nil } // Define a type for handler functions. type handlerFn func(ctx context.Context, request webhookRequest) ( webhookResponse, error) // Create a map from action to handler function. var handlers map[string]handlerFn = map[string]handlerFn{ "account.balance.check": accountBalanceCheck, } // 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 action from the request, and call the corresponding // function that handles that action. action := request.QueryResult.Action if fn, ok := handlers[action]; ok { response, err = fn(r.Context(), request) } else { err = fmt.Errorf("Unknown action: %s", action) } 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 } }
Click Deploy.
Wait until the status indicator shows that the function has successfully deployed. While waiting, examine the code you just deployed.
Configure the webhook for your agent
Now that the webhook exists as a service, you need to associate this webhook with your agent. This is done via fulfillment. To enable and manage fulfillment for your agent:
- Go to the Dialogflow ES console.
- Select the pre-built agent you just created.
- Select Fulfillment in the left sidebar menu.
- Toggle the Webhook field to Enabled.
- Provide the URL that you copied from above. Leave all other fields blank.
- Click Save at the bottom of the page.
Now that fulfillment is enabled for the agent, you need to enable fulfillment for an intent:
- Select Intents in the left sidebar menu.
- Select the account.balance.check intent.
- Scroll down to the Fulfillment section.
- Toggle Enable webhook call for this intent to on.
- Click Save.
Try the agent
Your agent is now ready to try. Click the Test Agent button to open the simulator. Attempt to have the following conversation with the agent:
Conversational turn | You | Agent |
---|---|---|
1 | Hello | Hello, thanks for choosing ACME Bank. |
2 | I want to know my account balance | What account do you want the balance for: savings or checking? |
3 | Checking | Here's your latest balance: $0.00 |
At conversational turn #3, you supplied "checking" as the account type. The account.balance.check intent has a parameter called account. This parameter is set to "checking" in this conversation. The intent also has an action value of "account.balance.check". The webhook service is called, and it is passed the parameter and action values.
If you examine the webhook code above, you see that this action triggers a similar named function to be called. The function determines the account balance. The function checks whether specific environment variables are set with information for connecting to the database. If these environment variables are not set, the function uses a hardcoded account balance. In upcoming steps, you will alter the environment for the function so that it retrieves data from a database.
Troubleshooting
The webhook code includes logging statements. If you are having issues, try viewing the logs for your function.
More information
For more information about the steps above, see: