개발자 소식으로 돌아가기

챗봇 경험에 WhatsApp Flows 추가하기

2024년 3월 20일제작:개피 G(Gafi G), 이리나 바그너(Iryna Wagner)

현재 그 어느 때보다 많은 비즈니스가 WhatsApp을 통한 챗봇을 사용하여 고객과 소통하고, 고객의 니즈를 파악하고, 필수 데이터를 수집하고 있습니다. 그러나 데이터를 수집하고 정리하는 방법은 복잡하고 효율성이 떨어질 수 있습니다. WhatsApp Flows가 바로 이 문제를 해결합니다.

챗봇에 WhatsApp Flows를 통합하면 수집한 고객 정보를 더 효과적으로 파악할 수 있습니다. 챗봇은 대화의 컨텍스트에 따라 데이터 수집을 위한 특정 플로를 시작합니다.

이 튜토리얼에서는 Llama 2와 Python을 사용하여 챗봇을 만들고 이를 WhatsApp Flows에 연결하여 데이터 수집 기능을 향상합니다. 이 과정에서 WhatsApp Flows가 어떤 식으로 챗봇 경험의 사용자 친숙도를 높이고 고객 데이터 수집의 정확성과 효율성을 개선하는지 알아볼 수 있습니다.

WhatsApp Flows로 챗봇 경험 향상하기

지금부터 만들 챗봇은 데이터 수집용 WhatsApp Flows를 사용하여 사용자 요청과 프롬프트에 응답합니다. 구체적으로는, 사용자가 챗봇과 대화를 나누고, 가상의 스페인 호텔에서 제공하는 서비스에 대한 정보를 검색하고, 호텔에 연락하여 지원을 받을 수 있도록 지원합니다.

챗봇은 간단한 if 문과 표준 Llama 2 모델을 사용하여 이 작업을 처리하는 데 필요한 정보가 담긴 지식 베이스에 대한 액세스를 제공합니다.

진행할 단계

파트 1:

파트 2:

  • Llama 2 모델을 GGML에서 GGUF로 변환합니다.

  • 플로와 챗봇을 통합하는 Python 코드를 작성합니다.
  • 메시지를 수신 대기하는 Webhook을 만듭니다.
  • 애플리케이션을 실행합니다.

파트 1: 플로를 만드는 방법

파트 1에서는 플로 빌드 도구를 사용하여 플로를 만듭니다. Flows API를 사용하여 플로를 만드는 방법도 있으나, 여기서는 다루지 않습니다.

파트 1 필수 조건

튜토리얼을 따라서 진행하려면 다음이 필요합니다.

마지막으로, Flows를 사용하기 위한 필수 단계를 완료해야 합니다. 완성된 프로젝트 코드를 미리 볼 수도 있습니다.

시작하기

시작하려면 WhatsApp Business 계정의 플로 페이지로 이동합니다. Flows를 사용하는 것이 처음이라면 플로 만들기 시작 버튼이 표시됩니다. 처음이 아니라면 페이지 오른쪽 상단에 플로 만들기 버튼이 표시됩니다.

표시된 버튼을 클릭하여 대화 상자를 열면 플로의 몇 가지 상세 정보를 입력할 수 있습니다.

WhatsApp Chatbox 시작하기

서비스 문의 플로

먼저 사용자가 호텔에서 제공하는 서비스에 대한 정보를 알아볼 수 있도록 지원하는 플로를 만들겠습니다.

이름 필드에 이름을 입력합니다. 그런 다음 카테고리 드롭다운에서 가입하기를 선택하고, 템플릿 드롭다운은 없음으로 남겨둡니다. 제출을 클릭합니다.

다음 페이지에는 왼쪽에 에디터가 표시되고 오른쪽에 미리 보기가 표시됩니다.

에디터에 입력된 내용을 플로의 JSON 마크업으로 바꿉니다. (플로 JSON에 대한 자세한 내용은 개발자 문서를 참조하세요.)

플로를 저장합니다. 미리 보기가 아래 이미지와 같이 표시됩니다.

Chatbox 플로

이 플로에는 사용자가 정보를 입력하고, 원하는 서비스를 선택하고, 선택적으로 추가 메시지를 입력할 수 있는 하나의 화면이 있습니다. 사용자가 제출을 클릭하면 플로가 닫히고 수집된 데이터가 처리될 수 있도록 비즈니스에 전송됩니다. 이 데이터를 전송하는 한 가지 방법은 엔드포인트를 사용하는 것입니다. 그러나 이 프로젝트에는 엔드포인트가 필요하지 않습니다. 데이터는 챗봇을 구동하는 것과 동일한 Webhook으로 전달됩니다.

이러한 데이터 전송은 액션을 사용하여 수행할 수 있습니다. payload 개체를 사용하여 다음 화면으로 전달할 데이터를 정의합니다.

...

"on-click-action": {
    "name": "complete",
    "payload": {
        "firstname": "${form.first_name}",
        "secondname": "${form.second_name}",
        "services_interested": "${form.services_interested}",
        "additional_info": "${form.additional_info}",
        "flow_key": "agentconnect"
    }
}
...
        

위 코드에서는 사용자의 버튼 클릭 액션이 on-click-action을 트리거하여 데이터를 페이로드에 수집합니다. 그런 다음 페이로드를 Webhook 서버로 전송하고 complete 액션을 통해 플로를 닫습니다.

payload 키는 변수를 할당하는 것처럼 할당하면 됩니다. 키에 대응되는 값은 데이터 개체 또는 플로 구성 요소의 이름(HTML 양식의 name 속성과 유사)을 나타낼 수 있습니다.

이제 인터랙티브 미리 보기 토글을 사용하여 플로가 실행되어 실제 사용자 경험을 시뮬레이션하는 것을 볼 수 있습니다.

Chatbox 인터랙티브 미리 보기

테스트를 마쳤으면 플로가 현재 임시 저장 상태에 있으므로 플로를 게시할 수 있습니다. 플로를 게시하려면 저장 오른쪽에 있는 메뉴를 열고 게시를 클릭합니다. 이제 플로를 사용할 수 있습니다.

문의하기 플로

이번에는 '문의하기' 플로를 만들겠습니다.

먼저 플로 만들기 프로세스를 반복합니다. 카테고리로 문의하기를 선택합니다. 에디터에 입력된 내용을 이 JSON 마크업으로 바꾸면 플로가 다음과 같이 렌더링됩니다.

Chatbox 문의하기 플로

플로를 게시하고, 챗봇을 설정하는 다음 섹션으로 넘어가세요. 다음 섹션에서는 사용자에게 플로를 전송하는 로직을 포함하는 세 가지 함수(send_message, flow_details, flow_reply_processor)를 사용합니다. 수신된 플로 페이로드를 처리하는 로직도 포함되어 있습니다. 이미 챗봇을 만든 경우에도 다음 섹션을 살펴보세요.

파트 2: 챗봇을 설정하는 방법

여기서는 챗봇을 구성하여 플로에 통합합니다.

파트 2 필수 조건

튜토리얼을 따라서 진행하려면 다음이 필요합니다.

  • 최신 버전의 Python과 Python에 대한 기본 지식

  • 다운로드한 HuggingFace 버전의 Llama 2. HuggingFace Llama 2 모델에는 추가적인 도구나 전용 하드웨어가 필요하지 않습니다. 공식 버전을 사용해도 되지만, 그러려면 추가 설정이 필요합니다.

  • 계정의 액세스 토큰과 전화번호 ID

  • 코드 에디터

Python을 사용하여 플로와 챗봇 통합하기

챗봇은 사용자의 입력을 바탕으로 사용자를 지원하도록 설계된 미리 정의된 스크립트에 따라 작동합니다. 최초 상호 작용에서는 맞춤화된 인사말 텍스트와 사용자의 메시지의 따른 텍스트 기반 메뉴가 표시됩니다. 이러한 옵션은 호텔에서 제공하는 서비스에 대해 문의하기, 호텔 상담원에게 문의하기, Llama 기반 챗봇 사용하기 등 사용자의 니즈에 맞게 표시됩니다.

영숫자 문자를 사용하여 응답하면 사용자가 해당 서비스 또는 액션으로 연결됩니다. 단, 영숫자 문자가 아닌 응답을 사용하면 일반적인 문의를 지원하거나 추가 대화를 바탕으로 사용 가능한 서비스를 안내하는 챗봇의 기본 기능이 트리거됩니다.

시작하려면 터미널에서 다음 명령을 실행하여 가상 환경을 만듭니다.

python -m venv venv
        

가상 환경을 활성화합니다.

source venv/bin/activate
        

그런 다음 필수 패키지를 설치합니다.

pip install requests flask llama-cpp-python python-dotenv
        

여기서 Flask는 경로를 만들고 API와 상호 작용하고, requests는 인터넷 요청을 전송하고, llama-cpp-python은 이 모델과 상호 작용하고, python-dotenv는 환경 변수를 읽어들입니다.

다음으로, 이름이 .env인 환경 파일을 만들고 다음과 같은 콘텐츠를 입력합니다. 적절한 값을 할당하세요. (TOKEN으로는 임의의 문자열을 사용해도 됩니다.)

TOKEN = 
ACCESS_TOKEN = 
PHONE_NUMBER_ID = 
        

동일한 디렉터리에서 이름이 main.py인 파일을 만들고 사용할 패키지를 추가합니다.

import os
import re
import time
import uuid
import requests
from dotenv import load_dotenv
from flask import Flask, request, make_response, json
from llama_cpp import Llama
        

이제 변수와 클래스를 초기화합니다. 다음 코드는 Flask를 초기화하고, load_dotenv() 메서드를 호출하여 변수를 읽어들입니다.

app = Flask(__name__)

load_dotenv()
PHONE_NUMBER_ID = os.getenv('PHONE_NUMBER_ID')
url = f"https://graph.facebook.com/v18.0/{PHONE_NUMBER_ID}/messages"
TOKEN = os.getenv('TOKEN')
ACCESS_TOKEN = os.getenv('ACCESS_TOKEN')

code_prompt_texts = ["Contact us", "Chat with our chatbot", "YES", "NO"]


service_list = [
    "Accommodation Services",
    "Spa Services",
    "Dining Services",
    "Recreational Facilities",
    "Business & Conference Services",
    "Transportation Services",
    "Accessibility Services",
    "Pet-Friendly Services"
]

service_list에는 호텔에서 제공하는 서비스가 저장됩니다. code_prompt_texts 리스트에는 사용자의 입력(1, 2, Y, N)에 대응되는 옵션이 들어 있습니다. 이때 아래의 함수가 사용됩니다. 이 함수는 사용자의 응답을 대응되는 옵션에 매핑합니다.

def extract_string_from_reply(user_input):
    match user_input:
        case "1":
            user_prompt = code_prompt_texts[0].lower()
        case "2":
            user_prompt = code_prompt_texts[1].lower()
        case "Y":
            user_prompt = code_prompt_texts[2].lower()
        case "N":
            user_prompt = code_prompt_texts[3].lower()
        case _:
            user_prompt = str(user_input).lower()

    return user_prompt
        

위 코드는 조건부 로직이 실행될 때 대/소문자로 인한 불일치가 발생하지 않도록 문자열을 소문자로 변환합니다. match…case 구조는 사용자가 입력한 프롬프트를 출력값에 매칭합니다. 예를 들어, 사용자가 '1'을 입력하면 "Contact us" 기능이 트리거됩니다.

다음으로, 아래의 함수에는 Python RegEx 패키지 re를 사용하여 고객 메시지에서 특정 용어를 검색하고 사용자에게 보낼 응답 유형을 정하는 if 문이 포함되어 있습니다.

def user_message_processor(message, phonenumber, name):
    user_prompt = extract_string_from_reply(message)
    if user_prompt == "yes":
        send_message(message, phonenumber, "TALK_TO_AN_AGENT", name)
    elif user_prompt == "no":
        print("Chat terminated")
    else:
        if re.search("service", user_prompt):
            send_message(message, phonenumber, "SERVICE_INTRO_TEXT", name)

        elif re.search(
            "help|contact|reach|email|problem|issue|more|information", user_prompt
        ):
            send_message(message, phonenumber, "CONTACT_US", name)

        elif re.search("hello|hi|greetings", user_prompt):
            if re.search("this", user_prompt):
                send_message(message, phonenumber, "CHATBOT", name)

            else:
                send_message(message, phonenumber, "SEND_GREETINGS_AND_PROMPT", name)

        else:
            send_message(message, phonenumber, "CHATBOT", name)
        

사용자가 "Hello there"와 같은 메시지를 입력했다면 두 번째 인수로 SEND_GREETINGS_AND_PROMPT가 지정된 상태로 send_message 메서드가 호출됩니다. 다음은 send_message 메서드입니다. <xxx> 안쪽을 적절한 값으로 바꾸세요.

def send_message(message, phone_number, message_option, name):
    greetings_text_body = (
        "\nHello "
        + name
        + ". Welcome to our hotel. What would you like us to help you with?\nPlease respond with a numeral between 1 and 2.\n\n1. "
        + code_prompt_texts[0]
        + "\n2. "
        + code_prompt_texts[1]
        + "\n\nAny other reply will connect you with our chatbot."
    )

    # loading the list's entries into a string for display to the user
    services_list_text = ""
    for i in range(len(service_list)):
        item_position = i + 1
        services_list_text = (
            f"{services_list_text} {item_position}. {service_list[i]} \n"
        )

    service_intro_text = f"We offer a range of services to ensure a comfortable stay, including but not limited to:\n\n{services_list_text}\n\nWould you like to connect with an agent to get more information about the services?\n\nY: Yes\nN: No"

    contact_flow_payload = flow_details(
        flow_header="Contact Us",
        flow_body="You have indicated that you would like to contact us.",
        flow_footer="Click the button below to proceed",
        flow_id=str("<FLOW-ID>"),
        flow_cta="Proceed",
        recipient_phone_number=phone_number,
        screen_id="CONTACT_US",
    )

    agent_flow_payload = flow_details(
        flow_header="Talk to an Agent",
        flow_body="You have indicated that you would like to talk to an agent to get more information about the services that we offer.",
        flow_footer="Click the button below to proceed",
        flow_id=str("<FLOW-ID>"),
        flow_cta="Proceed",
        recipient_phone_number=phone_number,
        screen_id="TALK_TO_AN_AGENT",
    )

    match message_option:
        case "SEND_GREETINGS_AND_PROMPT":
            payload = json.dumps(
                {
                    "messaging_product": "whatsapp",
                    "to": str(phone_number),
                    "type": "text",
                    "text": {"preview_url": False, "body": greetings_text_body},
                }
            )
        case "SERVICE_INTRO_TEXT":
            payload = json.dumps(
                {
                    "messaging_product": "whatsapp",
                    "to": str(phone_number),
                    "type": "text",
                    "text": {"preview_url": False, "body": service_intro_text},
                }
            )
        case "CHATBOT":
            LLM = Llama(
                model_path="/home/incognito/Downloads/llama-2-7b-chat.ggmlv3.q8_0.gguf.bin",
                n_ctx=2048,
            )
            # create a text prompt
            prompt = message
            # generate a response (takes several seconds)
            output = LLM(prompt)
            payload = json.dumps(
                {
                    "messaging_product": "whatsapp",
                    "to": str(phone_number),
                    "type": "text",
                    "text": {
                        "preview_url": False,
                        "body": output["choices"][0]["text"],
                    },
                }
            )
        case "CONTACT_US":
            payload = contact_flow_payload
        case "TALK_TO_AN_AGENT":
            payload = agent_flow_payload
        case "FLOW_RESPONSE":
            payload = json.dumps(
                {
                    "messaging_product": "whatsapp",
                    "to": str(phone_number),
                    "type": "text",
                    "text": {"preview_url": False, "body": message},
                }
            )

    headers = {
        "Content-Type": "application/json",
        "Authorization": "Bearer " + ACCESS_TOKEN,
    }

    requests.request("POST", url, headers=headers, data=payload)
    print("MESSAGE SENT")

메시지가 간단한 인사말(SEND_GREETINGS_AND_PROMPT)이면 응답에 추가적인 프롬프트(greetings_text_body)가 포함됩니다.

Chatbox 샘플 인사말

마찬가지로, 사용자가 호텔에서 제공하는 서비스에 대해 질문하면 서비스를 포함하는 텍스트 메시지(service_intro_text)가 사용자에게 전송됩니다. 여기에 더해 사용자에게 상담원과 대화하기를 원하는지 묻는 프롬프트도 포함됩니다.

Chatbox 샘플 안내

챗봇 응답이 필요한 경우(CHATBOT)에는 챗봇 모델을 초기화하고, 메시지 콘텐츠에 입력하고, 응답을 처리하여 사용자에게 전송합니다. FLOW_RESPONSE는 해당 플로에서 수집된 응답을 표시합니다.

나머지 옵션인 CONTACT_USTALK_TO_AN_AGENT는 사용자에게 플로 페이로드를 전송합니다. 이 플로 페이로드는 flow_details 함수에서 구성됩니다. 아래에는 함수의 본문이 나와 있습니다. 페이로드는 FLOW_ID를 비롯한 기본적인 플로 상세 정보로 구성되는데, 이 정보는 WhatsApp Business 계정의 플로 페이지에서 확인할 수 있습니다. 환경 변수 내에서 이러한 ID를 정의하는 것도 가능합니다.

def flow_details(flow_header, 
    flow_body, 
    flow_footer, 
    flow_id, 
    flow_cta, 
    recipient_phone_number, 
    screen_id
):
    # Generate a random UUID for the flow token
    flow_token = str(uuid.uuid4())

    flow_payload = json.dumps({
        "type": "flow",
        "header": {
            "type": "text",
            "text": flow_header
        },
        "body": {
            "text": flow_body
        },
        "footer": {
            "text": flow_footer
        },
        "action": {
            "name": "flow",
            "parameters": {
                "flow_message_version": "3",
                "flow_token": flow_token,
                "flow_id": flow_id,
                "flow_cta": flow_cta,
                "flow_action": "navigate",
                "flow_action_payload": {
                    "screen": screen_id
                }
            }
        }
    })

    payload = json.dumps({
        "messaging_product": "whatsapp",
        "recipient_type": "individual",
        "to": str(recipient_phone_number),
        "type": "interactive",
        "interactive": json.loads(flow_payload)
    })
    return payload
        

이 메서드는 랜덤 UUID를 생성하여 action.parameters.flow_token을 만듭니다. action.parameters.flow_action_payload.screen은 매개변수(screen_id)로 전달됩니다. 이 값은 action.parameters.flow_cta가 실행될 때 사용자에게 표시할 초기 화면의 ID를 나타내야 합니다.

마지막으로, Webhook 경로를 추가합니다. Webhook GET 요청은 Meta for Developers에 Webhook을 추가하는 시점에 초기화되고, 성공한 경우 요청의 hub.challenge를 반환합니다.

@app.route("/webhook", methods=["GET"])
def webhook_get():
    if request.method == "GET":
        if (
            request.args.get("hub.mode") == "subscribe"
            and request.args.get("hub.verify_token") == TOKEN
        ):
            return make_response(request.args.get("hub.challenge"), 200)
        else:
            return make_response("Success", 403)
        

POST 요청은 앞에서 소개한 user_message_processor 메서드를 사용하여 메시지 페이로드를 추출하고 처리합니다. 이 코드는 메시지 페이로드만 처리하기 때문에, 메시지가 아닌 페이로드가 캡처되면 오류가 발생합니다. 페이로드가 messages 본문이 존재하는지 확인하는 if 문을 사용할 수 있습니다.

@app.route("/webhook", methods=["POST"])
def webhook_post():
    if request.method == "POST":
        request_data = json.loads(request.get_data())
        if (
            request_data["entry"][0]["changes"][0]["value"].get("messages")
        ) is not None:
            name = request_data["entry"][0]["changes"][0]["value"]["contacts"][0][
                "profile"
            ]["name"]
            if (
                request_data["entry"][0]["changes"][0]["value"]["messages"][0].get(
                    "text"
                )
            ) is not None:
                message = request_data["entry"][0]["changes"][0]["value"]["messages"][
                    0
                ]["text"]["body"]
                user_phone_number = request_data["entry"][0]["changes"][0]["value"][
                    "contacts"
                ][0]["wa_id"]
                user_message_processor(message, user_phone_number, name)
            else:
                # checking that there is data in a flow's response object before processing it
                if (
                    request_data["entry"][0]["changes"][0]["value"]["messages"][0][
                        "interactive"
                    ]["nfm_reply"]["response_json"]
                ) is not None:
                    flow_reply_processor(request)

    return make_response("PROCESSED", 200)
        

이에 더해, 플로에서 응답을 추출하여 사용자에게 전송하는 도우미 함수 flow_reply_processor를 사용할 수 있습니다.

def flow_reply_processor(request):
    request_data = json.loads(request.get_data())
    name = request_data["entry"][0]["changes"][0]["value"]["contacts"][0]["profile"]["name"]
    message = request_data["entry"][0]["changes"][0]["value"]["messages"][0]["interactive"]["nfm_reply"][
        "response_json"]

    flow_message = json.loads(message)
    flow_key = flow_message["flow_key"]
    if flow_key == "agentconnect":
        firstname = flow_message["firstname"]
        reply = f"Thank you for reaching out {firstname}. An agent will reach out to you the soonest"
    else:
        firstname = flow_message["firstname"]
        secondname = flow_message["secondname"]
        issue = flow_message["issue"]
        reply = f"Your response has been recorded. This is what we received:\n\n*NAME*: {firstname} {secondname}\n*YOUR MESSAGE*: {issue}"

    user_phone_number = request_data["entry"][0]["changes"][0]["value"]["contacts"][0][
        "wa_id"]
    send_message(reply, user_phone_number, "FLOW_RESPONSE", name)
        

이 함수는 키(flow_key)를 사용하여 두 플로를 구분하여 적절한 응답을 추출하고 올바른 첫 번째 화면 ID를 전달합니다.

코드를 실행하기 전에 완성된 버전과 비교하여 모든 값이 일치하는지 확인하세요.

Webhook을 설정하는 방법

다음으로 넘어가기 전에 터미널에서 아래 명령을 실행합니다.

flask --app main run --port 5000 
        

명령이 성공하면 다음과 같은 메시지가 표시됩니다.

* Running on http://127.0.0.1:5000
        

다음으로, ngrok http 5000 명령을 실행하여 애플리케이션에 매핑되는 URL을 가져옵니다. 링크를 복사합니다.

그런 다음 Meta for Developers 개발자 계정에서 왼쪽 탐색 패널에 있는 WhatsApp 아래의 구성을 클릭합니다.

Chatbox 구성

Webhook 카드에서 수정을 클릭합니다.

그런 다음 열리는 대화 상자의 콜백 URL 필드에 복사한 URL을 붙여넣고 그 뒤에 /webhook을 추가합니다.

.env 파일의 TOKEN 변수에 들어 있는 토큰을 인증 토큰 필드에 추가합니다. 확인 및 저장을 클릭하여 대화 상자를 닫습니다.

Webhook 카드에서 관리를 클릭하고 메시지 필드를 선택합니다. 그러면 카드가 다음과 같이 표시됩니다.

WhatsApp Chatbox Webhook

이제 Webhook이 준비되었습니다.

애플리케이션 실행하기

본인의 계정 번호에 'Hello'와 같은 메시지를 보내보세요. 그러면 적절한 응답이 수신됩니다. 메뉴에 표시된 프롬프트로 답변하여 플로를 테스트해보세요.

WhatsApp Chatbox 앱 실행

다음은 챗봇의 응답을 보여주는 또 다른 스크린샷입니다. 사용자가 간단한 번역을 요청하자 챗봇이 적절하게 응답하는 것을 볼 수 있습니다.

WhatsApp Chatbox 앱 실행

결론

WhatsApp Flows는 비즈니스가 구조화된 정보를 수집하고 고객 상호 작용을 향상하고 비즈니스와 소비자 사이의 커뮤니케이션을 간소화하는 데 사용할 수 있는 강력한 도구입니다. 플로를 만드는 한 가지 방법은 사용자 친화적인 플로 디자인 인터페이스를 제공하는 WhatsApp 관리자 플로 빌드 도구를 사용하는 것입니다.

이 데모 애플리케이션을 통해 향상된 고객 응대와 데이터 기반 분석을 위해 WhatsApp Flows를 사용하는 방법을 알아볼 수 있었습니다. 보다 특수한 용도로 사용하려면 챗봇을 맞춤 모델에 연결하거나 독자적인 데이터 소스를 바탕으로 학습하도록 Llama 2와 WhatsApp Flows를 구성하여 제품에 대한 자연어 질문에 답변하고 그 밖의 기능을 제공하도록 할 수 있습니다.

지금 바로 WhatsApp Flows를 사용하여 고객 응대를 향상하고 애플리케이션을 통한 데이터 수집을 간소화해보세요. 지금 사용해보세요.