Watson으로 쉽게 만드는 카카오톡 ChatBot

이 포스팅은 4. Node.js 어플리케이션 카카오톡과 연동하기에 이어지는 포스팅입니다. 이 포스팅에서는 예약한 회의실 취소 기능을 추가함으로써 그동안 했던 내용을 모두 복습 및 응용해 보겠습니다.

< 이 포스팅과 연결된 글 목록 >

5. [응용] 회의실 예약 조회 및 취소 기능 추가하기

5.1 Watson Conversation 대화 서비스 만들기

Watson Conversation의 대화 서비스는 크게 Intent, Entity 그리고 Dialog를 생성함으로써 만들 수 있습니다. 자세한 내용은 1. Watson Conversation 서비스로 대화 서비스 만들기Watson Conversation에서 사용하는 Expression Language를 참조하십시오. 이 포스팅은 Tutorial 1에서 만든 서비스에 새로운 내용을 추가합니다. Tutorial 1을 완료하지 못한 분은 workspace에 링크에서 workspace-tutorial1.json을 다운로드 하여 import 하신 후에 진행하십시오.

먼저 Watson Conversation의 Tool에 접근합니다. Bluemix에 로그인하고 Watson Conversation 서비스를 추가한 조직과 영역의 페이지로 이동합니다. Conversation 서비스 이름을 클릭합니다. 관리 탭에서 Launch Tool 버튼을 클릭합니다.

5.1.1 Intent 만들기

3개의 Intent를 아래와 같이 추가합니다.

1) #Check_Reservation_List

intent1

2) #Cancel_Reservation

intent2

3) #Check_Next_Reservation

intent3

5.2.2 Entity 추가

Entity 화면의 System Entities 탭에서 @sys-number 엔티티를 활성화 시킵니다.

entity

5.2.3 Dialog 추가하기

Dialog 화면에서 아래와 같이 3개의 최상위 노드를 추가하십시오.

Index Triggered by Response Condition Response Jump to
5 #Check_Reservation_List
{
  "context": {
    "user": {
      "id": "hjjo\\@kr.ibm.com"
    },
    "action": {
      "command": "check-reservation"
    }
  },
  "output": {}
}
6 #Check_Next_Reservation
{
  "context": {
    "user": {
      "id": "hjjo\\@kr.ibm.com"
    },
    "action": {
      "command": "check-next-reservation"
    }
  },
  "output": {}
}
7 #Cancel_Reservation
{
  "context": {
    "user": {
      "id": "hjjo\\@kr.ibm.com"
    },
    "action": {
      "command": "check-reservation-for-cancellation"
    }
  },
  "output": {}
}

#Cancel_Reservation 노드 하위에 다음을 참조하여 노드를 추가하십시오.

Index Triggered by Response Condition Response Jump to
7-1 @sys-number
{
  "context": {
    "action": {
      "command": "confirm-cancellation"
    },
    "removeIndex": "< ? @sys-number - 1 ?>"
  },
  "output": {
    "text": {
      "values": [
        "Your reservation is successfully cancelled. Thanks !"
      ]
    }
  }
}
7-2 true Please try again :-D

이 튜토리얼을 모두 완료한 workspace를 import 하려면 링크에서 workspace-tutorial5.json을 다운로드 하십시오.

5.2 회의실 예약 조회 및 취소 Action 추가하기

이 포스팅에서는 기존 어플리케이션을 확장합니다. 앞선 튜토리얼을 완료하지 못하신 분은 tutorial5를 checkout하십시오.

git checkout tutorial5

/api/message.js를 수정하여 회의실 예약 조회 및 취소 Action을 추가하겠습니다.

1) doAction 함수의 case문에 다음의 케이스를 추가합니다.

let doAction = (data, action) => {
  console.log("Action : " + action.command);
  switch(action.command){
    case "check-availability":
      return checkAvailability(data, action);
      break;
    case "confirm-reservation":
      return confirmReservation(data, action);
      break;
    // 사용자의 예약 리스트를 가져옵니다.
    case "check-reservation":
      return checkReservation(data, action);
      break;
    // 사용자의 예약 리스트 중 가장 빠른 시간의 예약만 가져옵니다. 
    case "check-next-reservation":
      return checkNextReservation(data, action);
      break;
    // 예약 취소의 목적으로 예약 리스트를 가져옵니다.
    case "check-reservation-for-cancellation":
      return checkReservation(data, action).then(data => {
        if(Array.isArray(data.output.text)){
          data.output.text.unshift("Please tell me the number of the reservation you want to cancel.");
        }
        return data;
      });
      break;
    // 예약을 취소합니다.
    case "confirm-cancellation":
      return confirmCancellation(data, action);
      break;
    default: console.log("Command not supported.")
  }
}

2) checkReservation()함수를 아래와 같이 구현하여 추가합니다.

/** 
 * 사용자의 회의실 예약 리스트를 가져오는 함수
 * @param  {Object} data : response object
 * @param  {Object} action 
 */ 
let checkReservation = (data, action) => {
  // context에서 필요한 값을 추출합니다.
  let date = action.dates;
  let startTime, endTime;
  if(action.times){
    startTime = action.times[0]?action.times[0].value:undefined;
    endTime = action.times[1]?action.times[1].value:undefined;
  }

  // 날짜 값과 시간 값을 조합하여 시작 시간과 종료 시간을 Timestamp 형태로 변환합니다. 편의를 위해 종료 시간이 따로 명시되지 않는 경우 시작 시간에서 1개월 후로 설정하도록 합니다.
  let startTimestamp = new moment();
  if(startTime){
    startTimestamp = new moment(date+"T"+startTime+"+0900");
  }
  let endTimestamp = new moment(startTimestamp).month(startTimestamp.month() + 1);
  if(endTime){
    endTimestamp = new moment(date+" "+endTime);
  }

  // /book/search/byuser API는 site id, user id, start time, end time을 Query parameter로 받아 해당 시간에 사용자의 예약 리스트를 return해주는 api입니다.
  let reqOption = {
    method : 'GET',
    url : process.env.RBS_URL + '/book/search/byuser',
    headers : {
      'Accept': 'application/json',
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    qs : {
    "siteid" : "camomile",
    "userid" : data.context.user.id,
    "start" : startTimestamp.valueOf(),
    "end" : endTimestamp.valueOf()
    }
  };
  
  return new Promise((resolved, rejected) => {
    request(reqOption, (err, res, body) => {
      data.context.action = {};
      if(err){
        rejected(err);
      }
      console.log(reqOption, body);
      body = JSON.parse(body);
      // body 의 length가 0보다 크면 기존에 예약정보가 있다는 의미입니다.
      if(body && body.length > 0){
        let resvs = [];
        let index = 0;
        for(let resv of body){
          //예약 목록을 사용자가 볼 수 있는 형태로 변환하여 resvs 변수에 저장합니다.
          resvs.push((++index) + ": " + new moment(resv.start).format(config.dateTimeFormat) + " ~ " + new moment(resv.end).format(config.dateTimeFormat) + ", " + resv.roomid + ", " + resv.purpose);
        }
        //예약 목록을 Context에 저장합니다.
        data.context.reservations = body;
        //사용자에게 보여줄 예약 목록은 Output에 저장합니다.
        data.output.text = resvs;
      }
      else{
        data.output.text = ["Your reservation is not found."];
      }
      resolved(data);
    })
  });
}

3) checkNextReservation()함수를 아래와 같이 구현하여 추가합니다.

let checkNextReservation = (data, action) => {
  return checkReservation(data, action).then(data => {
    if(data.output.text && Array.isArray(data.output.text)) data.output.text = data.output.text[0];
    return data
  });
}

4) confirmCancellation()함수를 아래와 같이 구현하여 추가합니다.

/** 
 * 회의실 취소
 * @param  {Object} data : response object
 * @param  {Object} action 
 */ 
let confirmCancellation = (data, action) => {
  // user 정보는 action 정보에 담겨있지 않으므로 data에서 추출합니다.
  let user = data.context.user;
  let eventId = data.context.eventid;
  let reservations = data.context.reservations;
  let index = data.context.removeIndex;

  let reqOption = {
    method : 'DELETE',
    url : process.env.RBS_URL + '/book',
    headers : {
      'Accept': 'text/plain',//'application/json',
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    qs : {
      "eventid" : reservations[index].id,
      "userid" : user.id,
      "roomid" : reservations[index].roomid
    }
  };

  return new Promise((resolved, rejected) => {
    request(reqOption, (err, res, body) => {
      data.context.action = {};
      if (res.statusCode >= 300) {
        data.output.text = "Your request is not successful. Please try again."
      }
      resolved(data);
    })
  });
}

5.3 테스트

아래 명령을 실행하여 어플리케이션을 시작시키고 localhost:3000/ 에 접속하여 테스트합니다.

npm start

test

이어지는 포스팅은

Watson으로 쉽게 만드는 카카오톡 ChatBot 6. [심화] 예약 시간이 되면 ChatBot이 알려주기 (텔레그램)

입니다.