웹훅 서비스 만들기

모든 것이 에이전트에 하드코딩되므로 마지막 단계에서 만든 사전 빌드된 에이전트에서 계좌 잔액과 같은 동적 데이터를 제공할 수 없습니다. 이 튜토리얼 단계에서는 에이전트에 동적 데이터를 제공할 수 있는 웹훅을 만듭니다. Cloud Run 함수는 단순성으로 인해 이 튜토리얼에서 웹훅을 호스팅하는 데 사용되지만 웹훅 서비스를 호스팅할 수 있는 방법에는 여러 가지가 있습니다. 또한 이 예시에서는 Go 프로그래밍 언어를 사용하지만 Cloud Run Functions에서 지원하는 모든 언어를 사용할 수 있습니다.

함수 만들기

Cloud Run Functions는 Google Cloud 콘솔로 생성될 수 있습니다(문서 보기, 콘솔 열기). 이 튜토리얼에 사용할 함수를 만들려면 다음 안내를 따르세요.

  1. Dialogflow 에이전트 및 함수가 모두 동일한 프로젝트에 있어야 합니다. 이것은 Dialogflow의 함수 보안 액세스를 위한 가장 쉬운 방법입니다. 함수를 만들기 전에 Google Cloud 콘솔에서 프로젝트를 선택합니다.

    프로젝트 선택기로 이동

  2. Cloud Run Functions 개요 페이지를 엽니다.

    Cloud Run Functions 개요로 이동

  3. 함수 만들기를 클릭하고 다음 필드를 설정합니다.

    • 환경: 1세대
    • 함수 이름: tutorial-banking-webhook
    • 리전: 에이전트에 리전을 지정한 경우 동일한 리전 사용
    • HTTP 트리거 유형: HTTP
    • URL: 여기에서 복사 버튼을 클릭하고 값을 저장합니다. 웹훅을 구성할 때 이 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. 상태 표시기에 함수가 성공적으로 배포된 것으로 표시될 때까지 기다립니다. 기다리는 동안 바로 전에 배포한 코드를 검사합니다.

에이전트를 위한 웹훅 구성

이제 웹훅이 서비스로 존재하므로 이 웹훅을 에이전트에 연결해야 합니다. 이 작업은 fulfillment를 통해 수행됩니다. 에이전트의 fulfillment를 사용 설정하고 관리하려면 다음 안내를 따르세요.

  1. Dialogflow ES 콘솔로 이동합니다.
  2. 방금 만든 사전 빌드된 에이전트를 선택합니다.
  3. 왼쪽 사이드바 메뉴에서 Fulfillment를 선택합니다.
  4. 웹훅 필드를 사용 설정됨으로 전환합니다.
  5. 위에서 복사한 URL을 입력합니다. 다른 모든 필드를 비워둡니다.
  6. 페이지 하단의 저장을 클릭합니다.

fulfillment 사용 설정 스크린샷

이제 에이전트에 fulfillment가 사용 설정되었으므로 인텐트에 fulfillment를 사용 설정해야 합니다.

  1. 왼쪽 사이드바 메뉴에서 인텐트를 선택합니다.
  2. account.balance.check 인텐트를 선택합니다.
  3. Fulfillment 섹션까지 아래로 스크롤합니다.
  4. 이 인텐트에 웹훅 호출 사용 설정을 활성화로 전환합니다
  5. 저장을 클릭합니다.

에이전트 사용해 보기

이제 에이전트를 사용해 볼 수 있습니다. 테스트 에이전트 버튼을 클릭하여 시뮬레이터를 엽니다. 에이전트와 다음 대화를 시도해 봅니다.

대화 차례 상담사
1 안녕하세요 ACME Bank를 이용해 주셔서 감사합니다.
2 계좌 잔액을 확인하고 싶습니다. 저축 예금 계좌와 당좌 계좌 중 어떤 계좌의 잔액을 확인하고 싶으신가요?
3 당좌예금 현재 잔액은 $0.00입니다.

대화 차례 3번에서 계좌 유형으로 '당좌'를 제공했습니다. account.balancing.check 인텐트에는 account라는 매개변수가 있습니다. 이 매개변수는 이 대화에서 '당좌'로 설정됩니다. 이 인텐트에는 'account.balancing.check'라는 작업 값도 있습니다. 웹훅 서비스가 호출되고 매개변수 값과 작업 값이 전달됩니다.

위의 웹훅 코드를 살펴보면 이 작업이 유사한 이름의 함수를 호출하는 것을 확인할 수 있습니다. 이 함수는 계좌 잔액을 결정합니다. 이 함수는 데이터베이스에 연결하는 데 필요한 정보로 특정 환경 변수가 설정되었는지 확인합니다. 이러한 환경 변수가 설정되지 않으면 함수에서 하드코딩된 계좌 잔액을 사용합니다. 다음 단계에서는 데이터베이스에서 데이터를 가져오도록 함수의 환경을 변경합니다.

문제 해결

웹훅 코드에는 로깅 구문이 포함되어 있습니다. 문제가 있으면 함수의 로그를 확인해 보세요.

추가 정보

위 단계에 대한 자세한 내용은 다음을 참조하세요.