스티밋 보팅 챗봇 만들기 - (4) 서버에서 Dialogflow 연동하기

in kr •  last month

서버에서 Steem API 개발이 완료되었고, 이제는 Dialogflow와 이를 연동해볼 차례입니다.

이번 포스트에서는 Dialogflow 웹훅을 현 서버의 프로세스 엔드포인트와 연동해보겠습니다. 이미 앞서, Steem API를 작성하였기 때문에 이를 연동하는 작업은 그리 어렵지 않을 것입니다.

우선, 새로운 파일인 dialogflow.js 를 만들고, 이를 메인 app.js에서 연결합니다.

-dialogflow.js

module.exports = function(app, key) {
    const request = require('request');     
}


-app.js

const express = require('express');
const bodyParser = require('body-parser');
const fs = require('fs');
const app = express();
app.use(bodyParser.urlencoded({limit: '50mb', extended: true}));
app.use(bodyParser.json());

app.use((err, req, res, next) => next());

const key = JSON.parse(fs.readFileSync('./.key', 'utf8'));

require('./steemapi')(app, key);
require('./dialogflow')(app, key);

const server = app.listen(3000, function(){
  console.log("Express server has started on port 3000")
});

메인 3000포트로 서버를 실행하고, dialogflow.js에 express의 레퍼런스값과 key 오브젝트 값을 전달합니다.

dialogflow.js에서 다음과 같이 /process에 대한 POST 통신을 위한 엔드포인트로 코드를 구현합니다.

app.post('/process', (req, res) => {
    const body = req.body;  

    console.log(JSON.stringify(body));
        
    if(!body){
        notUnderstood(res);
        return;
    }

        const intentName = body.result.metadata.intentName;

        if(intentName=='view_intent'){              

        } else if(intentName=='vote_intent'){
                    
} else {
            console.log(`  No intent matched.`);
            notUnderstood(res);
        }

});

해당 코드는 Dialogflow V1 API를 기반으로 작성되었습니다. 따라서, Dialogflow 콘솔의 설정으로 들어가, API version > V1 API 가 체크되었는지 확인해보세요.

스크린샷 2018-07-13 오후 2.25.47.png

다시, 앞서 코드를 살펴보면, Dialogflow로부터 웹훅을 통해 전달된 분석된 Object 값을 가지고 해당 인텐트명과 연관 엔티티 등을 추출하면 됩니다.

해당 인텐트 명은

const intentName = body.result.metadata.intentName; 

으로 가져올 수 있고, 이는 우리가 Dialogflow에서 설정했던 인텐트명 이름과 같습니다. 이 인텐트 명을 ‘view_intent’ 인지, ‘vote_intent’인지에 따라 로직을 분기하면 됩니다.

만약, 해당 요청값이 없거나 어느 인텐트에도 속하지 않는다면, notUnderstood 함수를 호출해줍니다.

const notUnderstood = (res) => {        
  const msg = '죄송해요. 알아듣지 못했어요. 다시 말해줄래요?';
  res.json({ 'speech': msg, 'displayText': msg });
}

여기서, 응답값의 경우 이와 같은 포맷으로 전달하여 추후, dialogflow와 다른 서드파티와 연동되었을 때, 해당 문장을 출력하도록 합니다. 이에 대한 요청 및 응답값에 대한 상세정보는 다음 링크(https://dialogflow.com/docs/fulfillment)에 잘 나와있습니다.

만약, 인식된 인텐트 명이 ‘view_intent’라면,

if(intentName=='view_intent'){              
        const discuss_option = body.result.parameters.discuss_option;

        actionOnViewIntent(discuss_option, res);
}

인식된 엔티티인 discuss_option을 actionOnViewIntent 함수에 넘겨줍니다.

const actionOnViewIntent = (discuss_option, res) => {
    const st_query = {"tag": "kr", "limit": 10};
    if(discuss_option=='최신'){
        app.getDiscussionsByCreated(st_query).then((result) => {
            let msg = "";
            result.forEach((entry) => {
                const url = "https://steemit.com/"+entry.url;
                msg += entry.title +"\n URL: "+url;
            });
                    
            res.json({ 'speech': msg, 'displayText': msg });
        }).catch((err) => {
            notUnderstood(res);
        });
    } else if(discuss_option=='핫한'){                    
        app.getDiscussionsByHot(st_query).then((result) => {                        
            let msg = "";
            result.forEach((entry) => {
                const url = "https://steemit.com/"+entry.url;
                msg += entry.title +"\n URL: "+url;
            });
                
            res.json({ 'speech': msg, 'displayText': msg });                                        
        }).catch((err) => {
            notUnderstood(res);
        }); 
    }
}

이미 정의한 discuss_option의 엔티티로서, ‘최신'인지, ‘핫한'인지에 따라 다른 Steem API를 불러오게 되지요. 물론, 이 API는 전 포스트에서 이미 만들어 보았습니다.

‘최신'이면, app.getDiscussionsByCreated() 함수를 호출하고, ‘핫한'이면 app.getDiscussionsByHot() 함수를 호출합니다. 이에 따라 나오는 결과 배열을 하나의 문자열로 합치고 JSON 형태로 반환하게 됩니다.

인텐트가 ‘vote_intent’이라면, 보팅 관련한 액션을 수행해야겠지요.

} else if(intentName=='vote_intent'){
const tag_option = body.result.parameters.tag_option;

if(tag_option=='태그') {
const resolvedQuery = body.result.resolvedQuery;
        const arrTags = resolvedQuery.split(" ").filter((value) => value != tag_option);

        if(arrTags[0]){
            actionOnVoteIntent(arrTags[0], res);
        } else {                    
            notUnderstood(res);
        }
    }       
}

정의한 엔티티인 tag_option이 ‘태그'인 경우, 해당 액션을 수행하게됩니다. 다른 액션의 경우도 이와 같은 방식으로 처리할 수 있겠죠.

이는 앞서 ‘view_intent’를 처리한 방식과 다른데. resolvedQuery는 유저가 쿼리한 전체 문장을 가져오는 것입니다. 즉, 그 문장 중에서 ‘태그'를 제외한 나머지 단어를 추출하는 것인데요. 그러면, 쿼리할 태그 단어를 가져오게 됩니다. 이는 Dialogflow에 학습시키지 않았기에, 직접 문장을 가지고 추출할 것입니다. 이 부분은 추후에 변경될 여지가 있을 수도 있어요. 아무튼, 이 태그가 있으면, actionOnVoteIntent 함수로 해당 태그를 전달합니다.

    const actionOnVoteIntent = (tag, res) => {      
        app.getDiscussionsByTag(tag).then((post) => {
            console.log(JSON.stringify(post));

            app.vote(post, 100).then((post) => {
                app.comment(tag, post).then((post) => {     
                
                    let msg = `${tag}의 자동 보팅이 완료되었습니다! `;
                    const url = "https://steemit.com/"+post.url;
                    msg += post.title +"\n URL: "+url;                          

                    res.json({ 'speech': msg, 'displayText': msg });            
                }).catch((err) => {
                    const errorMsg = `다음과 같은 이유로, 프로세스가 완료되지 못했습니다 : ${err}`;
                    res.json({ 'speech': errorMsg, 'displayText': errorMsg});
                });
            }).catch((err) => {
                const errorMsg = `다음과 같은 이유로, 프로세스가 완료되지 못했습니다 : ${err}`;
                res.json({ 'speech': errorMsg, 'displayText': errorMsg});
            });
        }).catch((err) => {
            console.error(err);
            notUnderstood(res);
        });                     
    }

앞서 만든 API인 app.getDiscussionsByTag 에 태그를 전달하고, 이에 대한 태그를 가진 최신 포스트가 반환되면, app.vote(post, 100) 를 통해, 보팅을 합니다. 여기서 100은 보팅율로 임의로 넣은 값입니다. 보팅이 성공하면, 차례로 app.comment(tag, post)를 통해, 해당 포스트에 코멘트를 달게 되고, 이것이 성공하면, JSON 형태로 결과를 반환합니다. 오류에 따른 에러 메시지도 같은 방식으로 반환하구요.

이제, 서버를 실행해보고, 테스트를 위해서 Postman 등의 REST용 리퀘스트 도구를 사용하는 방법이 있는데, 여기서는 생략하겠습니다. 또한, Firebase Cloud Function을 활용하여 구축하면, 서버 배포가 편리하게 이루어집니다. 이 내용들은 ‘누구나 쉽게 배우는 챗봇 서비스' 책을 참고하면 보다 자세한 내용을 학습할 수 있습니다.

이제 서버에 배포하여 해당 엔드포인트에 접근할 수 있다고 가정하고, 이어가보겠습니다.

Dialogflow 콘솔에 다시 들어가서, 우리가 앞서 만든 에이전트의 Fulfilment 페이지로 들어가봅니다.

거기서, Webhook을 활성화하고, URL 인풋란에 배포한 서버 URL의 엔드포인트를 입력합니다. 이는 앞서 만든 path 인 /process 가 되겠죠.

스크린샷 2018-07-13 오후 2.20.32.png

이제, 하단의 Save 버튼을 누르고, 오른쪽의 Try it now 란을 통해 테스트를 진행해볼게요.

‘최신순 포스트' 라는 쿼리를 입력하고 엔터를 쳐 봅시다.

그러면, 앞서 테스트할 때 아무런 응답값이 없었던 Response 란에 다음과 같이 응답 내용이 나오는 것을 확인할 수 있습니다.

스크린샷 2018-07-13 오전 11.27.37.png

이는 해당 쿼리가 Dialogflow에 분석된 후, 우리가 연동한 웹훅 서버를 통해 전달되고, 이를 처리하여 나온 결과물입니다.
아래, Inline Editor은 Firebase Cloud Function을 통해 구현할 수 있는 웹훅이며, 여기에 코드를 입력하는 것 만으로도 빠르게 웹훅 서버를 구축할 수 있습니다.

보팅도 비슷한 방식으로 테스트할 수 있습니다. ‘태그 kr-dev’와 같은 방식으로 테스트해보세요. 만약, 잘 안되면, 보팅율을 조정하는 방식으로 해결할 수 있습니다.

이번 포스트에서 기 구축된 서버 API와 Dialogflow를 연동하고, 테스트해보았습니다. 이제 거의 마무리 단계에 왔네요~!

다음 마지막 포스트에서는 Dialogflow의 integration 을 활용하여 커뮤니케이션 메신저인 슬랙과 통합해보겠습니다.

  1. 스티밋 보팅 챗봇 만들기 - 개요
  2. 스티밋 보팅 챗봇 만들기 - DialogFlow 로 학습하기
  3. 스티밋 보팅 챗봇 만들기 - 서버에서 Steem API 구성하기
  4. 스티밋 보팅 챗봇 만들기 - 서버에서 DialogFlow 연동하기
  5. 스티밋 보팅 챗봇 만들기 - 슬랙으로 통합하기

http://bit.ly/2tQKV9J : 누구나 쉽게 배우는 챗봇 서비스

보다 관심있는 분들은 이 책에서 자세한 내용을 확인할 수 있습니다~.

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!
Sort Order:  

저도 하루 빨리 저 코드들을 완벽히 이해할 수 있는 날이 왔으면 좋겠네요 ㅎ 좋은 글 감사합니다!

·

모르는 부분이 있으면 언제든지 물어봐주세요~ :)