Webhook サービスを作成する

前のステップで作成したビルド済みエージェントでは、アカウント残高などの動的データを提供することはできません。これは、すべてのデータがエージェントにハードコードされているためです。チュートリアルのこのステップでは、動的データをエージェントに提供できる Webhook を作成します。このチュートリアルでは、そのシンプルさから、Webhook をホストするために Cloud Run 関数を使用しますが、Webhook サービスはその他のさまざまな方法でホストできます。この例では Go プログラミング言語も使用していますが、Cloud Run 関数でサポートされている言語を使用できます。

関数の作成

Cloud Run 関数は Google Cloud コンソールで作成できます(ドキュメントに移動して、コンソールを開きます)。このチュートリアルの関数を作成するには:

  1. Dialogflow エージェントと関数の両方が同じプロジェクト内にあることが重要です。これが、Dialogflow が関数に安全にアクセスするための最も簡単な方法です。関数を作成する前に、Google Cloud コンソールからプロジェクトを選択します。

    プロジェクト セレクタに移動

  2. Cloud Run 関数の概要ページを開きます。

    Cloud Run 関数の概要に移動

  3. [関数を作成] をクリックして、次のフィールドを設定します。

    • 環境: 第 1 世代
    • 関数名: tutorial-banking-webhook
    • リージョン: エージェントにリージョンを指定した場合は、同じリージョンを使用します。
    • HTTP トリガータイプ: HTTP
    • URL: ここのコピーボタンをクリックして、値を保存します。Webhook を構成するときに、この URL が必要になります。
    • 認証: 認証が必要です
    • HTTPS が必須: チェックボックスをオンにする
  4. [保存] をクリックします。

  5. [次へ] をクリックします(特別なランタイム、ビルド、接続、セキュリティ設定は必要ありません)。

  6. 次のフィールドを設定します。

    • ランタイム: 最新の Go ランタイムを選択します。
    • ソースコード: インライン エディタ
    • エントリ ポイント: HandleWebhookRequest
  7. コードを次のように置き換えます。

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

  8. [デプロイ] をクリックします。

  9. ステータス インジケーターが関数が正常にデプロイされたことを示すまで待ちます。待機中に、デプロイしたコードを確認します。

エージェントの Webhook を構成する

Webhook がサービスとして存在するようになったため、この Webhook をエージェントに関連付ける必要があります。これはフルフィルメントにより行います。エージェントのフルフィルメントを有効化および管理する手順は次のとおりです。

  1. Dialogflow ES コンソールに移動します。
  2. 作成した事前構築済みエージェントを選択します。
  3. 左側のサイドバー メニューで [Fulfillment] を選択します。
  4. [Webhook] フィールドを [Enabled] に切り替えます。
  5. 上記でコピーした URL を入力します。その他のフィールドは空欄のままにします。
  6. ページの下部にある [保存] をクリックします。

フルフィルメントを有効にするスクリーンショット。

これでエージェント用にフルフィルメントが有効になったため、インテントのフルフィルメントを有効にする必要があります。

  1. 左側のサイドバーのメニューで [Intents] を選択します。
  2. account.balance.check インテントを選択します。
  3. [Fulfillment] セクションまで下にスクロールします。
  4. [Enable webhook call for this intent] をオンにします。
  5. [保存] をクリックします。

エージェントを試す

エージェントを試す準備が整いました。[Test Agent] ボタンをクリックして、シミュレータを開きます。 エージェントとの次の会話を試行します。

会話ターン 受講者 エージェント
1 こんにちは ACME 銀行をご利用いただきありがとうございます。
2 アカウントの残高を知りたいです 普通預金と当座預金、どちらの口座の残高をご希望ですか?
3 当座 最新の残高は $0.00 です。

会話ターン #3 で、口座の種類として「当座預金」を指定しました。account.balance.check インテントには account というパラメータがあります。このパラメータは、この会話では「checking」に設定されています。このインテントには、アクション値として「account.balance.check」も設定されています。Webhook サービスが呼び出され、パラメータとアクションの値が渡されます。

前述の Webhook コードを調べると、このアクションによって類似の名前付き関数の呼び出しがトリガーされることがわかります。この関数は、口座残高を決定します。この関数は、データベースに接続するための情報が特定の環境変数に設定されているかどうかを確認します。これらの環境変数が設定されていない場合、この関数はハードコードされた口座残高を使用します。以降の手順では、データベースからデータを取得するように関数の環境を変更します。

トラブルシューティング

Webhook コードにはログ ステートメントが含まれています。問題が発生した場合は、関数のログを表示してみてください。

詳細

上述の手順の詳細については、以下をご覧ください。