Dialogflow 에이전트를 React 애플리케이션에 통합
게시 됨: 2022-03-10Dialogflow는 Dialogflow 콘솔이나 통합 웹 애플리케이션에서 사용할 때 음성 또는 텍스트 입력을 처리할 수 있는 자연어 처리 대화형 채팅 도우미를 만들고 설계하는 프로세스를 단순화하는 플랫폼입니다.
이 문서에서는 통합 Dialogflow 에이전트에 대해 간략하게 설명하지만 Node.js 및 Dialogflow에 대해 이해하고 있어야 합니다. Dialogflow에 대해 처음 배우는 경우 이 문서에서 Dialogflow가 무엇인지와 개념에 대해 명확하게 설명합니다.
이 문서는 React.js 웹 애플리케이션과 에이전트 간의 링크인 Express.js 백엔드 애플리케이션을 사용하여 웹 애플리케이션에 통합할 수 있는 음성 및 채팅 지원을 통해 Dialogflow 에이전트를 구축한 방법에 대한 가이드입니다. Dialogflow 자체에서. 기사가 끝나면 자신의 Dialogflow 에이전트를 원하는 웹 애플리케이션에 연결할 수 있을 것입니다.
이 가이드를 쉽게 따라갈 수 있도록 튜토리얼에서 가장 관심 있는 부분으로 건너뛰거나 표시되는 순서대로 따라가면 됩니다.
- Dialogflow 에이전트 설정
- Dialogflow 에이전트 통합
- Node Express 애플리케이션 설정
- Dialogflow로 인증
- 서비스 계정이란 무엇입니까?
- 음성 입력 처리
- 웹 애플리케이션에 통합
- 채팅 인터페이스 만들기
- 사용자 음성 입력 녹음
- 결론
- 참고문헌
1. Dialogflow 에이전트 설정
이 문서에서 설명한 대로 Dialogflow의 채팅 도우미는 에이전트라고 하며 인텐트, 이행, 지식 기반 등과 같은 더 작은 구성요소로 구성됩니다. Dialogflow는 사용자가 에이전트의 대화 흐름을 만들고 학습시키고 설계할 수 있는 콘솔을 제공합니다. 이 사용 사례에서는 에이전트 내보내기 및 가져오기 기능을 사용하여 교육 후 ZIP 폴더로 내보낸 에이전트를 복원합니다.
가져오기를 수행하기 전에 복원하려는 에이전트와 병합될 새 에이전트를 생성해야 합니다. 콘솔에서 새 에이전트를 만들려면 고유한 이름과 에이전트를 연결할 Google Cloud의 프로젝트가 필요합니다. 연결할 Google Cloud에 기존 프로젝트가 없으면 여기에서 새 프로젝트를 만들 수 있습니다.
에이전트는 예산에 따라 사용자에게 와인 제품을 추천하기 위해 이전에 생성되고 교육되었습니다. 이 에이전트는 ZIP으로 내보내졌습니다. 여기에서 폴더를 다운로드하고 에이전트 설정 페이지에 있는 내보내기 및 가져오기 탭에서 새로 생성된 에이전트로 복원할 수 있습니다.
가져온 에이전트는 사용자의 와인 한 병 구매 예산을 기반으로 사용자에게 와인 제품을 추천하도록 이전에 교육을 받았습니다.
가져온 에이전트를 살펴보면 인텐트 페이지에서 3개의 생성된 인텐트가 있음을 알 수 있습니다. 하나는 Agent가 사용자의 입력을 인식하지 못할 때 사용하는 fallback Intent이고, 다른 하나는 Agent와 대화가 시작될 때 사용하는 Welcome Intent이며, 마지막 Intent는 사용자에게 와인을 추천하기 위해 사용됩니다. 문장 내의 amount 매개변수. 우리가 우려하는 것은 get-wine-recommendation
의도입니다.
이 인텐트에는 대화를 이 인텐트에 연결하기 위해 기본 환영 wine-recommendation
의 단일 입력 컨텍스트가 있습니다.
"컨텍스트는 한 의도에서 다른 의도로의 대화 흐름을 제어하는 데 사용되는 에이전트 내의 시스템입니다."
컨텍스트 아래에는 사용자에게 어떤 유형의 진술을 기대해야 하는지에 대해 에이전트를 교육하는 데 사용되는 문장인 교육 문구가 있습니다. 인텐트 내의 다양한 학습 문구를 통해 에이전트는 사용자의 문장과 해당 문장이 속하는 인텐트를 인식할 수 있습니다.
에이전트 get-wine-recommendation
의도 내의 교육 문구(아래 참조)는 와인 선택 및 가격 범주를 나타냅니다.
위의 이미지를 보면 사용 가능한 교육 문구가 나열되어 있고 각 항목에 대해 통화 수치가 노란색으로 강조 표시되어 있습니다. 이 강조 표시는 Dialogflow에서 주석으로 알려져 있으며 사용자 문장에서 엔터티로 알려진 인식된 데이터 유형을 추출하기 위해 자동으로 수행됩니다.
에이전트와의 대화에서 이 인텐트가 일치된 후 사용자의 문장에서 매개변수로 추출된 가격을 기반으로 권장 와인을 얻기 위해 외부 서비스에 HTTP 요청이 만들어집니다. 이 인텐트 페이지 하단의 이행 섹션.
Dialogflow 콘솔의 오른쪽 섹션에 있는 Dialogflow 에뮬레이터를 사용하여 에이전트를 테스트할 수 있습니다. 테스트를 위해 " 안녕하세요 " 메시지로 대화를 시작하고 원하는 양의 와인을 따라갑니다. Webhook이 즉시 호출되고 에이전트가 아래와 유사한 풍부한 응답을 표시합니다.
위의 이미지에서 Ngrok을 사용하여 생성된 웹훅 URL과 사용자가 입력한 $20 가격대의 와인을 보여주는 오른쪽 에이전트의 응답을 볼 수 있습니다.
이 시점에서 Dialogflow 에이전트가 완전히 설정되었습니다. 이제 이 에이전트를 웹 애플리케이션에 통합하여 다른 사용자가 Dialogflow 콘솔에 액세스하지 않고도 에이전트에 액세스하고 상호 작용할 수 있도록 시작할 수 있습니다.
Dialogflow 에이전트 통합
REST 엔드포인트에 HTTP 요청을 하는 것과 같이 Dialogflow 에이전트에 연결하는 다른 방법이 있지만 Dialogflow에 연결하는 권장 방법은 여러 프로그래밍 언어로 제공되는 공식 클라이언트 라이브러리를 사용하는 것입니다. JavaScript의 경우 @google-cloud/dialogflow 패키지를 NPM에서 설치할 수 있습니다.
내부적으로 @google-cloud/dialogflow 패키지는 네트워크 연결에 gRPC를 사용하므로 웹팩을 사용하여 패치하는 경우를 제외하고 브라우저 환경 내에서 패키지가 지원되지 않습니다. 이 패키지를 사용하는 데 권장되는 방법은 노드 환경입니다. 이 패키지를 사용하도록 Express.js 백엔드 애플리케이션을 설정한 다음 API 엔드포인트를 통해 웹 애플리케이션에 데이터를 제공함으로써 이를 수행할 수 있습니다. 이것이 우리가 다음에 할 일입니다.
Node Express 애플리케이션 설정
익스프레스 애플리케이션을 설정하기 위해 새 프로젝트 디렉토리를 만든 다음 열린 명령줄 터미널에서 yarn
를 사용하여 필요한 종속성을 가져옵니다.
# create a new directory and ( && ) move into directory mkdir dialogflow-server && cd dialogflow-server # create a new Node project yarn init -y # Install needed packages yarn add express cors dotenv uuid
필요한 종속성이 설치되면 웹 앱에 대해 활성화된 CORS 지원으로 지정된 포트에서 연결을 처리하는 매우 간결한 Express.js 서버를 설정할 수 있습니다.
// index.js const express = require("express") const dotenv = require("dotenv") const cors = require("cors") dotenv.config(); const app = express(); const PORT = process.env.PORT || 5000; app.use(cors()); app.listen(PORT, () => console.log(` server running on port ${PORT}`));
실행되면 위 스니펫의 코드는 지정된 PORT Express.js에서 연결을 수신 대기하는 HTTP 서버를 시작합니다. 또한 cors 패키지를 Express 미들웨어로 사용하는 모든 요청에 대해 CORS(교차 출처 리소스 공유)가 활성화되어 있습니다. 현재 이 서버는 연결만 수신 대기하고 있으며 생성된 경로가 없기 때문에 요청에 응답할 수 없으므로 이것을 생성해 보겠습니다.
이제 두 개의 새로운 경로를 추가해야 합니다. 하나는 텍스트 데이터를 전송하고 다른 하나는 녹음된 음성 입력을 전송하는 것입니다. 둘 다 POST
요청을 수락하고 요청 본문에 포함된 데이터를 나중에 Dialogflow 에이전트로 보냅니다.
const express = require("express") const app = express() app.post("/text-input", (req, res) => { res.status(200).send({ data : "TEXT ENDPOINT CONNECTION SUCCESSFUL" }) }); app.post("/voice-input", (req, res) => { res.status(200).send({ data : "VOICE ENDPOINT CONNECTION SUCCESSFUL" }) }); module.exports = app
위에서 우리는 현재로서는 200
상태 코드와 하드코딩된 더미 응답으로만 응답하는 두 개의 생성된 POST
경로에 대해 별도의 라우터 인스턴스를 만들었습니다. Dialogflow로 인증을 마치면 이러한 엔드포인트 내에서 Dialogflow에 대한 실제 연결을 구현하기 위해 돌아올 수 있습니다.
백엔드 애플리케이션 설정의 마지막 단계에서 app.use와 경로의 기본 경로를 사용하여 이전에 생성된 라우터 인스턴스를 Express 애플리케이션에 마운트합니다.
// agentRoutes.js const express = require("express") const dotenv = require("dotenv") const cors = require("cors") const Routes = require("./routes") dotenv.config(); const app = express(); const PORT = process.env.PORT || 5000; app.use(cors()); app.use("/api/agent", Routes); app.listen(PORT, () => console.log(` server running on port ${PORT}`));
위에서 우리는 두 경로에 기본 경로를 추가했습니다. 두 경로 중 하나는 명령줄에서 cURL을 사용하여 POST
요청을 통해 테스트할 수 있습니다.
curl -X https://localhost:5000/api/agent/text-response
위의 요청이 성공적으로 완료되면 객체 데이터가 포함된 응답이 콘솔에 출력되는 것을 기대할 수 있습니다.
이제 @google-cloud/dialogflow 패키지를 사용하여 Dialogflow의 에이전트에서 인증, 전송 및 데이터 수신을 처리하는 것을 포함하여 Dialogflow와 실제 연결하는 작업만 남았습니다.
Dialogflow로 인증
생성된 모든 Dialogflow 에이전트는 GCP의 프로젝트에 연결됩니다. Dialogflow 에이전트에 외부적으로 연결하기 위해 Google 클라우드에서 프로젝트로 인증하고 Dialogflow를 프로젝트 리소스 중 하나로 사용합니다. Google 클라우드에서 프로젝트에 연결하는 6가지 방법 중 서비스 계정 옵션을 사용하는 것이 클라이언트 라이브러리를 통해 Google 클라우드의 특정 서비스에 연결할 때 가장 편리합니다.
참고 : 프로덕션 준비 애플리케이션의 경우 서비스 계정 키가 잘못된 사람에게 들어갈 위험을 줄이기 위해 서비스 계정 키보다 수명이 짧은 API 키를 사용하는 것이 좋습니다.
서비스 계정이란 무엇입니까?
서비스 계정은 대부분 외부 API를 통해 사람이 아닌 상호작용을 위해 생성된 Google Cloud의 특별한 유형의 계정입니다. 우리 애플리케이션에서 서비스 계정은 Dialogflow 클라이언트 라이브러리에서 생성된 키를 통해 액세스하여 GCP에 인증합니다.
서비스 계정 생성 및 관리에 대한 클라우드 문서는 서비스 계정 생성에 대한 훌륭한 가이드를 제공합니다. 서비스 계정을 생성할 때 마지막 단계와 같이 생성된 서비스 계정에 Dialogflow API 관리자 역할을 할당해야 합니다. 이 역할은 연결된 Dialogflow 에이전트에 대한 관리 제어 권한을 서비스 계정에 부여합니다.
서비스 계정을 사용하려면 서비스 계정 키를 만들어야 합니다. 다음 단계에서는 JSON 형식으로 생성하는 방법을 간략하게 설명합니다.
- 새로 생성된 서비스 계정을 클릭하여 서비스 계정 페이지로 이동합니다.
- 키 섹션으로 스크롤하고 키 추가 드롭다운을 클릭하고 새 키 만들기 옵션을 클릭하여 모달을 엽니다.
- JSON 파일 형식을 선택하고 모달 오른쪽 하단의 만들기 버튼을 클릭합니다.
참고: 서비스 계정 키를 비공개로 유지하고 버전 제어 시스템 에 커밋하지 않는 것이 좋습니다 . 여기 에는 GCP의 프로젝트에 대한 매우 민감한 데이터가 포함되어 있기 때문입니다. .gitignore
파일에 파일을 추가하면 됩니다.
서비스 계정이 생성되고 프로젝트 디렉터리 내에서 사용 가능한 서비스 계정 키가 있으면 Dialogflow 클라이언트 라이브러리를 사용하여 Dialogflow 에이전트에서 데이터를 보내고 받을 수 있습니다.
// agentRoute.js require("dotenv").config(); const express = require("express") const Dialogflow = require("@google-cloud/dialogflow") const { v4 as uuid } = require("uuid") const Path = require("path") const app = express(); app.post("/text-input", async (req, res) => { const { message } = req.body; // Create a new session const sessionClient = new Dialogflow.SessionsClient({ keyFilename: Path.join(__dirname, "./key.json"), }); const sessionPath = sessionClient.projectAgentSessionPath( process.env.PROJECT_ID, uuid() ); // The dialogflow request object const request = { session: sessionPath, queryInput: { text: { // The query to send to the dialogflow agent text: message, }, }, }; // Sends data from the agent as a response try { const responses = await sessionClient.detectIntent(request); res.status(200).send({ data: responses }); } catch (e) { console.log(e); res.status(422).send({ e }); } }); module.exports = app;
위의 전체 경로는 Dialogflow 에이전트에 데이터를 보내고 다음 단계를 통해 응답을 받습니다.
- 첫 번째
Google 클라우드로 인증한 다음 Dialogflow 에이전트에 연결된 Google 클라우드 프로젝트의 projectID와 생성된 세션을 식별하기 위한 임의의 ID를 사용하여 Dialogflow와의 세션을 생성합니다. 우리 애플리케이션에서는 JavaScript UUID 패키지를 사용하여 생성된 각 세션에 UUID 식별자를 생성합니다. 이는 Dialogflow 에이전트가 처리하는 모든 대화를 기록하거나 추적할 때 매우 유용합니다. - 초
Dialogflow 문서에 지정된 형식에 따라 요청 객체 데이터를 생성합니다. 이 요청 객체에는 생성된 세션과 Dialogflow 에이전트에 전달할 요청 본문에서 가져온 메시지 데이터가 포함됩니다. - 제삼
Dialogflow 세션의detectIntent
메서드를 사용하여 요청 개체를 비동기적으로 보내고 try-catch 블록에서 ES6 async/await 구문을 사용하여 에이전트의 응답을 기다립니다.detectIntent
메서드가 예외를 반환하면 오류를 잡아서 반환할 수 있습니다. 전체 응용 프로그램을 충돌시키는 것보다. 에이전트에서 반환된 응답 개체의 샘플은 Dialogflow 설명서에 제공되며 개체에서 데이터를 추출하는 방법을 알기 위해 검사할 수 있습니다.
Postman을 사용하여 dialogflow-response
경로에서 위에 구현된 Dialogflow 연결을 테스트할 수 있습니다. Postman은 개발 또는 생산 단계에서 빌드된 API를 테스트하는 기능을 갖춘 API 개발을 위한 협업 플랫폼입니다.
참고: 아직 설치하지 않은 경우 API를 테스트하는 데 Postman 데스크탑 애플리케이션이 필요하지 않습니다. 2020년 9월부터 Postman의 웹 클라이언트는 GA(일반 사용 가능) 상태로 전환되었으며 브라우저에서 직접 사용할 수 있습니다.
Postman 웹 클라이언트를 사용하여 새 작업 공간을 만들거나 기존 작업 공간을 사용하여 https://localhost:5000/api/agent/text-input
의 API 끝점에 대한 POST
요청을 만들고 키가 있는 데이터를 추가할 수 있습니다. message
와 " Hi There "의 값을 쿼리 매개변수에 입력합니다.
보내기 버튼을 클릭하면 실행 중인 Express 서버에 POST
요청이 이루어지며 아래 이미지와 유사한 응답이 표시됩니다.
위의 이미지에서 Express 서버를 통해 Dialogflow 에이전트의 예쁜 응답 데이터를 볼 수 있습니다. 반환된 데이터는 Dialogflow Webhook 문서에 제공된 샘플 응답 정의에 따라 형식이 지정됩니다.
음성 입력 처리
기본적으로 모든 Dialogflow 에이전트는 텍스트와 오디오 데이터를 모두 처리하고 텍스트 또는 오디오 형식으로 응답을 반환할 수 있습니다. 그러나 오디오 입력 또는 출력 데이터로 작업하는 것은 텍스트 데이터보다 약간 더 복잡할 수 있습니다.
음성 입력을 처리하고 처리하기 위해 오디오 파일을 수신하고 에이전트의 응답에 대한 대가로 Dialogflow로 보내기 위해 이전에 만든 /voice-input
엔드포인트에 대한 구현을 시작합니다.
// agentRoutes.js import { pipeline, Transform } from "stream"; import busboy from "connect-busboy"; import util from "promisfy" import Dialogflow from "@google-cloud/dialogflow" const app = express(); app.use( busboy({ immediate: true, }) ); app.post("/voice-input", (req, res) => { const sessionClient = new Dialogflow.SessionsClient({ keyFilename: Path.join(__dirname, "./recommender-key.json"), }); const sessionPath = sessionClient.projectAgentSessionPath( process.env.PROJECT_ID, uuid() ); // transform into a promise const pump = util.promisify(pipeline); const audioRequest = { session: sessionPath, queryInput: { audioConfig: { audioEncoding: "AUDIO_ENCODING_OGG_OPUS", sampleRateHertz: "16000", languageCode: "en-US", }, singleUtterance: true, }, }; const streamData = null; const detectStream = sessionClient .streamingDetectIntent() .on("error", (error) => console.log(error)) .on("data", (data) => { streamData = data.queryResult }) .on("end", (data) => { res.status(200).send({ data : streamData.fulfillmentText }} }) detectStream.write(audioRequest); try { req.busboy.on("file", (_, file, filename) => { pump( file, new Transform({ objectMode: true, transform: (obj, _, next) => { next(null, { inputAudio: obj }); }, }), detectStream ); }); } catch (e) { console.log(`error : ${e}`); } });
높은 개요에서 위의 /voice-input
경로는 채팅 도우미에게 말하는 메시지가 포함된 파일로 사용자의 음성 입력을 수신하고 이를 Dialogflow 에이전트로 보냅니다. 이 프로세스를 더 잘 이해하기 위해 다음과 같은 작은 단계로 나눌 수 있습니다.
- 먼저 connect-busboy를 웹 애플리케이션의 요청으로 전송되는 양식 데이터를 구문 분석하기 위한 Express 미들웨어로 추가하고 사용합니다. 그런 다음 서비스 키를 사용하여 Dialogflow에 인증하고 이전 경로에서 했던 것과 동일한 방식으로 세션을 만듭니다.
그런 다음 내장된 Node.js 유틸리티 모듈의 promisify 메서드를 사용하여 나중에 여러 스트림을 파이프하고 스트림이 완료된 후 정리를 수행하는 데 사용할 Stream 파이프라인 메서드와 동일한 약속을 가져와 저장합니다. - 다음으로 Dialogflow 인증 세션과 Dialogflow로 전송될 오디오 파일의 구성을 포함하는 요청 객체를 생성합니다. 중첩된 오디오 구성 개체를 사용하면 Dialogflow 에이전트가 전송된 오디오 파일에 대해 Speech-To-Text 변환을 수행할 수 있습니다.
- 다음으로 생성된 세션과 요청 객체를 사용하여 Dialogflow 에이전트에서 백엔드 애플리케이션으로 새 데이터 스트림을 여는
detectStreamingIntent
메서드를 사용하여 오디오 파일에서 사용자의 의도를 감지합니다. 데이터는 이 스트림을 통해 작은 비트로 다시 전송되며 읽을 수 있는 스트림의 " 이벤트 " 데이터를 사용하여 나중에 사용할 수 있도록streamData
변수에 데이터를 저장합니다. 스트림이 닫힌 후 " end " 이벤트가 발생하고streamData
변수에 저장된 Dialogflow 에이전트의 응답을 웹 애플리케이션으로 보냅니다. - 마지막으로 connect-busboy의 파일 스트림 이벤트를 사용하여 요청 본문에서 전송된 오디오 파일의 스트림을 수신하고 이전에 생성한 Pipeline에 해당하는 Promise로 더 전달합니다. 이것의 기능은 요청에서 들어오는 오디오 파일 스트림을 Dialogflow 스트림으로 파이프하는 것입니다. 우리는 오디오 파일 스트림을 위의
detectStreamingIntent
메소드에 의해 열린 스트림으로 파이프합니다.
위의 단계가 계획대로 작동하는지 테스트하고 확인하기 위해 Postman을 사용하여 /voice-input
엔드포인트에 대한 요청 본문에 오디오 파일이 포함된 테스트 요청을 만들 수 있습니다.
위의 Postman 결과는 요청 본문에 포함된 " Hi "라고 녹음된 음성 메모 메시지의 양식 데이터로 POST 요청을 수행한 후 얻은 응답을 보여줍니다.
이 시점에서 이제 Dialogflow에서 데이터를 보내고 받는 기능적인 Express.js 애플리케이션이 생겼습니다. 이 기사의 두 부분은 완료되었습니다. 이제 Reactjs 애플리케이션에서 생성된 API를 사용하여 이 에이전트를 웹 애플리케이션에 통합하는 일만 남았습니다.
웹 애플리케이션에 통합
빌드된 REST API를 사용하기 위해 이미 API에서 가져온 와인 목록을 보여주는 홈 페이지와 babel 제안 데코레이터 플러그인을 사용하는 데코레이터 지원이 있는 이 기존 React.js 애플리케이션을 확장합니다. 상태 관리를 위한 Mobx와 Express.js 애플리케이션에서 추가된 REST API 끝점을 사용하여 채팅 구성 요소에서 와인을 추천하는 새로운 기능을 도입하여 약간 리팩토링합니다.
시작하려면 몇 가지 관찰 가능한 값과 일부 메서드를 작업으로 사용하여 Mobx 저장소를 만들면서 MobX를 사용하여 애플리케이션 상태를 관리하기 시작합니다.
// store.js import Axios from "axios"; import { action, observable, makeObservable, configure } from "mobx"; const ENDPOINT = process.env.REACT_APP_DATA_API_URL; class ApplicationStore { constructor() { makeObservable(this); } @observable isChatWindowOpen = false; @observable isLoadingChatMessages = false; @observable agentMessages = []; @action setChatWindow = (state) => { this.isChatWindowOpen = state; }; @action handleConversation = (message) => { this.isLoadingChatMessages = true; this.agentMessages.push({ userMessage: message }); Axios.post(`${ENDPOINT}/dialogflow-response`, { message: message || "Hi", }) .then((res) => { this.agentMessages.push(res.data.data[0].queryResult); this.isLoadingChatMessages = false; }) .catch((e) => { this.isLoadingChatMessages = false; console.log(e); }); }; } export const store = new ApplicationStore();
위에서 우리는 다음 값을 갖는 응용 프로그램 내에서 채팅 구성 요소 기능에 대한 저장소를 만들었습니다.
-
isChatWindowOpen
여기에 저장된 값은 Dialogflow의 메시지가 표시되는 채팅 구성요소의 가시성을 제어합니다. -
isLoadingChatMessages
Dialogflow 에이전트에서 응답을 가져오기 위한 요청이 있을 때 로드 표시기를 표시하는 데 사용됩니다. -
agentMessages
이 배열은 Dialogflow 에이전트로부터 응답을 받기 위해 만들어진 요청에서 오는 모든 응답을 저장합니다. 배열의 데이터는 나중에 구성 요소에 표시됩니다. -
handleConversation
액션으로 데코레이팅된 이 메서드는 데이터를agentMessages
배열에 추가합니다. 먼저 전달된 사용자 메시지를 인수로 추가한 다음 Axios를 사용하여 백엔드 애플리케이션에 요청하여 Dialogflow에서 응답을 받습니다. 요청이 해결되면 요청의 응답을agentMessages
배열에 추가합니다.
참고: 애플리케이션에서 데코레이터 를 지원하지 않는 경우 MobX는 대상 저장소 클래스의 생성자에서 사용할 수 있는 makeObservable을 제공 합니다. 여기 에서 예를 참조 하십시오.
스토어 설정을 통해 index.js
파일의 루트 구성 요소에서 시작하는 MobX Provider 고차 구성 요소로 전체 애플리케이션 트리를 래핑해야 합니다.
import React from "react"; import { Provider } from "mobx-react"; import { store } from "./state/"; import Home from "./pages/home"; function App() { return ( <Provider ApplicationStore={store}> <div className="App"> <Home /> </div> </Provider> ); } export default App;
위에서 루트 App 구성 요소를 MobX Provider로 래핑하고 이전에 만든 저장소를 Provider 값 중 하나로 전달합니다. 이제 상점에 연결된 구성 요소 내에서 상점에서 읽을 수 있습니다.
채팅 인터페이스 만들기
API 요청에서 보내거나 받은 메시지를 표시하려면 나열된 메시지를 표시하는 일부 채팅 인터페이스가 있는 새 구성 요소가 필요합니다. 이를 위해 먼저 일부 하드 코딩된 메시지를 표시한 다음 나중에 메시지를 정렬된 목록에 표시하는 새 구성 요소를 만듭니다.
// ./chatComponent.js import React, { useState } from "react"; import { FiSend, FiX } from "react-icons/fi"; import "../styles/chat-window.css"; const center = { display: "flex", jusitfyContent: "center", alignItems: "center", }; const ChatComponent = (props) => { const { closeChatwindow, isOpen } = props; const [Message, setMessage] = useState(""); return ( <div className="chat-container"> <div className="chat-head"> <div style={{ ...center }}> <h5> Zara </h5> </div> <div style={{ ...center }} className="hover"> <FiX onClick={() => closeChatwindow()} /> </div> </div> <div className="chat-body"> <ul className="chat-window"> <li> <div className="chat-card"> <p>Hi there, welcome to our Agent</p> </div> </li> </ul> <hr style={{ background: "#fff" }} /> <form onSubmit={(e) => {}} className="input-container"> <input className="input" type="text" onChange={(e) => setMessage(e.target.value)} value={Message} placeholder="Begin a conversation with our agent" /> <div className="send-btn-ctn"> <div className="hover" onClick={() => {}}> <FiSend style={{ transform: "rotate(50deg)" }} /> </div> </div> </form> </div> </div> ); }; export default ChatComponent
위의 구성 요소에는 채팅 응용 프로그램에 필요한 기본 HTML 마크업이 있습니다. 에이전트의 이름과 채팅 창을 닫기 위한 아이콘을 보여주는 헤더, 목록 태그에 하드코딩된 텍스트를 포함하는 메시지 풍선, 마지막으로 입력된 텍스트를 저장하기 위해 입력 필드에 첨부된 onChange
이벤트 핸들러가 있는 입력 필드가 있습니다. React의 useState를 사용하여 구성 요소의 로컬 상태.
위 이미지에서 채팅 구성 요소는 정상적으로 작동하며 하단에 단일 채팅 메시지와 입력 필드가 있는 스타일이 지정된 채팅 창을 보여줍니다. 그러나 우리는 메시지가 하드코딩된 텍스트가 아닌 API 요청에서 얻은 실제 응답으로 표시되기를 원합니다.
Chat 구성 요소를 리팩터링하기 위해 진행합니다. 이번에는 구성 요소 내의 MobX 저장소에 있는 값을 연결하고 사용합니다.
// ./components/chatComponent.js import React, { useState, useEffect } from "react"; import { FiSend, FiX } from "react-icons/fi"; import { observer, inject } from "mobx-react"; import { toJS } from "mobx"; import "../styles/chat-window.css"; const center = { display: "flex", jusitfyContent: "center", alignItems: "center", }; const ChatComponent = (props) => { const { closeChatwindow, isOpen } = props; const [Message, setMessage] = useState(""); const { handleConversation, agentMessages, isLoadingChatMessages, } = props.ApplicationStore; useEffect(() => { handleConversation(); return () => handleConversation() }, []); const data = toJS(agentMessages); return ( <div className="chat-container"> <div className="chat-head"> <div style={{ ...center }}> <h5> Zara {isLoadingChatMessages && "is typing ..."} </h5> </div> <div style={{ ...center }} className="hover"> <FiX onClick={(_) => closeChatwindow()} /> </div> </div> <div className="chat-body"> <ul className="chat-window"> {data.map(({ fulfillmentText, userMessage }) => ( <li> {userMessage && ( <div style={{ display: "flex", justifyContent: "space-between", }} > <p style={{ opacity: 0 }}> . </p> <div key={userMessage} style={{ background: "red", color: "white", }} className="chat-card" > <p>{userMessage}</p> </div> </div> )} {fulfillmentText && ( <div style={{ display: "flex", justifyContent: "space-between", }} > <div key={fulfillmentText} className="chat-card"> <p>{fulfillmentText}</p> </div> <p style={{ opacity: 0 }}> . </p> </div> )} </li> ))} </ul> <hr style={{ background: "#fff" }} /> <form onSubmit={(e) => { e.preventDefault(); handleConversation(Message); }} className="input-container" > <input className="input" type="text" onChange={(e) => setMessage(e.target.value)} value={Message} placeholder="Begin a conversation with our agent" /> <div className="send-btn-ctn"> <div className="hover" onClick={() => handleConversation(Message)} > <FiSend style={{ transform: "rotate(50deg)" }} /> </div> </div> </form> </div> </div> ); }; export default inject("ApplicationStore")(observer(ChatComponent));
위 코드의 강조 표시된 부분에서 전체 채팅 구성 요소가 다음과 같은 새로운 작업을 수행하도록 수정되었음을 알 수 있습니다.
-
ApplicationStore
값을 주입한 후 MobX 저장소 값에 액세스할 수 있습니다. 또한 구성 요소는 이러한 저장 값의 관찰자가 되어 값 중 하나가 변경될 때 다시 렌더링됩니다. - 구성 요소가 렌더링되는 즉시 요청을 만들기 위해
useEffect
후크 내에서handleConversation
메서드를 호출하여 채팅 구성 요소가 열린 직후 에이전트와 대화를 시작합니다. - 이제 Chat 구성 요소 헤더 내에서
isLoadingMessages
값을 사용하고 있습니다. 에이전트로부터 응답을 받기 위한 요청이 진행 중일 때isLoadingMessages
값을true
로 설정하고 헤더를 Zara가 입력하고 있음으로 업데이트합니다… - 저장소 내의
agentMessages
배열은 값이 설정된 후 MobX에 의해 프록시로 업데이트됩니다. 이 구성 요소에서 MobX의toJS
유틸리티를 사용하여 해당 프록시를 다시 배열로 변환하고 구성 요소 내의 변수에 값을 저장합니다. 이 배열은 map 함수를 사용하여 배열 내의 값으로 채팅 거품을 채우기 위해 추가로 반복됩니다.
이제 채팅 구성 요소를 사용하여 문장을 입력하고 응답이 에이전트에 표시될 때까지 기다릴 수 있습니다.
사용자 음성 입력 녹음
기본적으로 모든 Dialogflow 에이전트는 사용자의 지정된 언어로 된 음성 또는 텍스트 기반 입력을 수락할 수 있습니다. 그러나 사용자의 마이크에 액세스하고 음성 입력을 녹음하려면 웹 응용 프로그램에서 약간의 조정이 필요합니다.
이를 달성하기 위해 HTML MediaStream 녹음 API를 사용하여 MobX 저장소의 두 가지 새로운 방법 내에서 사용자의 음성을 녹음하도록 MobX 저장소를 수정합니다.
// store.js import Axios from "axios"; import { action, observable, makeObservable } from "mobx"; class ApplicationStore { constructor() { makeObservable(this); } @observable isRecording = false; recorder = null; recordedBits = []; @action startAudioConversation = () => { navigator.mediaDevices .getUserMedia({ audio: true, }) .then((stream) => { this.isRecording = true; this.recorder = new MediaRecorder(stream); this.recorder.start(50); this.recorder.ondataavailable = (e) => { this.recordedBits.push(e.data); }; }) .catch((e) => console.log(`error recording : ${e}`)); }; };
채팅 구성 요소에서 녹음 아이콘을 클릭하면 위의 MobX 저장소에 있는 startAudioConversation
메서드가 호출되어 관찰 가능한 isRecording
속성이 true 로 설정되어 채팅 구성 요소가 녹음이 진행 중임을 표시하는 시각적 피드백을 제공할 수 있습니다.
브라우저의 네비게이터 인터페이스를 사용하여 Media Device 개체에 액세스하여 사용자의 장치 마이크를 요청합니다. getUserMedia
요청에 권한이 부여되면 사용자의 장치 마이크에서 반환된 스트림의 미디어 트랙을 사용하여 레코더를 생성하기 위해 MediaRecorder 생성자에 추가로 전달하는 MediaStream 데이터로 약속을 확인합니다. 그런 다음 나중에 다른 메서드에서 액세스할 것이므로 미디어 레코더 인스턴스를 저장소의 recorder
속성에 저장합니다.
다음으로, 레코더 인스턴스에서 start 메소드를 호출하고, 레코딩 세션이 종료된 후, 우리가 기록된Bits 배열 속성에 저장한 Blob에 recordedBits
된 스트림을 포함하는 이벤트 인수와 함께 ondataavailable
함수가 시작됩니다.
발생된 ondataavailable
이벤트에 전달된 이벤트 인수의 데이터를 로그아웃하면 브라우저 콘솔에서 Blob과 해당 속성을 볼 수 있습니다.
이제 MediaRecorder 스트림을 시작할 수 있으므로 사용자가 음성 입력 녹음을 완료하고 생성된 오디오 파일을 Express.js 애플리케이션으로 보낼 때 MediaRecorder 스트림을 중지할 수 있어야 합니다.
아래 저장소에 추가된 새 메서드는 스트림을 중지하고 녹음된 음성 입력을 포함하는 POST
요청을 만듭니다.
//store.js import Axios from "axios"; import { action, observable, makeObservable, configure } from "mobx"; const ENDPOINT = process.env.REACT_APP_DATA_API_URL; class ApplicationStore { constructor() { makeObservable(this); } @observable isRecording = false; recorder = null; recordedBits = []; @action closeStream = () => { this.isRecording = false; this.recorder.stop(); this.recorder.onstop = () => { if (this.recorder.state === "inactive") { const recordBlob = new Blob(this.recordedBits, { type: "audio/mp3", }); const inputFile = new File([recordBlob], "input.mp3", { type: "audio/mp3", }); const formData = new FormData(); formData.append("voiceInput", inputFile); Axios.post(`${ENDPOINT}/api/agent/voice-input`, formData, { headers: { "Content-Type": "multipart/formdata", }, }) .then((data) => {}) .catch((e) => console.log(`error uploading audio file : ${e}`)); } }; }; } export const store = new ApplicationStore();
The method above executes the MediaRecorder's stop method to stop an active stream. Within the onstop
event fired after the MediaRecorder is stopped, we create a new Blob with a music type and append it into a created FormData.
As the last step., we make POST
request with the created Blob added to the request body and a Content-Type: multipart/formdata
added to the request's headers so the file can be parsed by the connect-busboy middleware from the backend-service application.
With the recording being performed from the MobX store, all we need to add to the chat-component is a button to execute the MobX actions to start and stop the recording of the user's voice and also a text to show when a recording session is active.
import React from 'react' const ChatComponent = ({ ApplicationStore }) => { const { startAudiConversation, isRecording, handleConversation, endAudioConversation, isLoadingChatMessages } = ApplicationStore const [ Message, setMessage ] = useState("") return ( <div> <div className="chat-head"> <div style={{ ...center }}> <h5> Zara {} {isRecording && "is listening ..."} </h5> </div> <div style={{ ...center }} className="hover"> <FiX onClick={(_) => closeChatwindow()} /> </div> </div> <form onSubmit={(e) => { e.preventDefault(); handleConversation(Message); }} className="input-container" > <input className="input" type="text" onChange={(e) => setMessage(e.target.value)} value={Message} placeholder="Begin a conversation with our agent" /> <div className="send-btn-ctn"> {Message.length > 0 ? ( <div className="hover" onClick={() => handleConversation(Message)} > <FiSend style={{ transform: "rotate(50deg)" }} /> </div> ) : ( <div className="hover" onClick={() => handleAudioInput()} > <FiMic /> </div> )} </div> </form> </div> ) } export default ChatComponent
From the highlighted part in the chat component header above, we use the ES6 ternary operators to switch the text to “ Zara is listening …. ” whenever a voice input is being recorded and sent to the backend application. This gives the user feedback on what is being done.
Also, besides the text input, we added a microphone icon to inform the user of the text and voice input options available when using the chat assistant. If a user decides to use the text input, we switch the microphone button to a Send button by counting the length of the text stored and using a ternary operator to make the switch.
We can test the newly connected chat assistant a couple of times by using both voice and text inputs and watch it respond exactly like it would when using the Dialogflow console!
결론
In the coming years, the use of language processing chat assistants in public services will have become mainstream. This article has provided a basic guide on how one of these chat assistants built with Dialogflow can be integrated into your own web application through the use of a backend application.
The built application has been deployed using Netlify and can be found here. Feel free to explore the Github repository of the backend express application here and the React.js web application here. They both contain a detailed README to guide you on the files within the two projects.
참고문헌
- Dialogflow Documentation
- Building A Conversational NLP Enabled Chatbot Using Google's Dialogflow by Nwani Victory
- 몹X
- https://web.postman.com
- Dialogflow API: Node.js Client
- Using the MediaStream Recording API