주문을 하거나, 시나리오가 좀 복잡한 캡슐을 개발하다 보면, 예외처리가 필요한 경우가 있습니다.

 

특정 액션을 수행하다가, 뭔가 문제가 발생했을때 (exception) 다른 액션을 대신 수행 시키는 방법입니다.

 

fail library의 checkedError 함수와 replan을 활용하는 것인데요.

 

replan을 하면, 다른 액션을 수행시킬 수 있습니다만, 조건이 있습니다.

 

A의 액션 수행중 B 액션으로 replan을 할 경우, A와 B의 아웃풋 컨셉이 동일해야 합니다.

+ 그리고 B의 아웃풋 컨셉이 A의 아웃풋 컨셉을 extends 한 컨셉일 때도 가능합니다. (반대는 안됨)

 

활용 예시를 하나 들어서 캡슐을 만들어 볼 건데요.

 

마트에서 주문을 시작하면, 메뉴 리스트를 보여주고(ShowMenu), continue 발화로 카트에 담는 것(AddItem)입니다.

메뉴 리스트에서 메뉴를 담으면, 자동으로 카트 화면을 보여주고(ShowCart) 카트에는 현재 담겨져 있는 메뉴들을 보여줍니다.

 

! 그런데, 메뉴 리스트에 없는 아이템을 말하면 (exception), 해당 메뉴는 없다는 메시지와 함께, 메뉴 화면을 다시 보여주게 하고자 합니다. (replan ShowMenu)

 

그래서, ShowMenu, ShowCart, AddItem 액션 모두 output 컨셉은 Order 라고 정의를 했습니다.

(Transaction 캡슐을 만드시게 되면 보통 이렇게 합니다.)

 

그리고 CreateOrder 라는 액션으로 초기 Order 컨셉을 생성합니다. 메뉴 리스트도 여기서 만들어 두고 있습니다.

 


메뉴를 담는 액션인 AddItem 액션 모델링입니다.

action (AddItem) {
  description (__DESCRIPTION__)
  type (Search)
  collect {
    input (order) {
      type (Order)
      min(Required) max(One)
      default-init {
        intent {
          goal : CreateOrder
        }
      }
    }
    input (userRequest) {
      type (UserRequest)
      min (Required) max (One)
    }
  }
  output (Order) {
    throws {
      error (GoShowCart) {
        property (resultOrder) {
          type (Order)
          min (Required) max (One)
        }
        on-catch {
          replan {
            intent {
              goal : ShowCart
              value : $expr(resultOrder)
            }
          }
        }
      }
      error (GoShowMenu) {
        property (resultOrder) {
          type (Order)
          min (Required) max (One)
        }
        on-catch {
          replan {
            intent {
              goal : ShowMenu
              value : $expr(resultOrder)
            }
          }
        }
      }
    }
  }
}

Order 컨셉은 일종의 Context 또는 주문정보 자체라고 보시면 되고, 이 컨셉을 액션간에 계속 넘겨주면서 정보를 유지하게 합니다.

AddItem 액션은, Order와 userRequest (주문요청 정보)를 받아서 처리하는데요.

Output 블럭 안에 throw 블럭을 유심히 보셔야 합니다. js 로직에서 던지는 exception을 받아서 처리하는 부분입니다.

 

AddItem.js 로직을 보겠습니다.

 

function addItem (order, userRequest) {

  if (!order.orderedItems) {
    order.orderedItems = [];
  }
  if (userRequest) {
    if (!userRequest.quantity) {
      userRequest.quantity = 1;
    }

    var itemExist = false;
    if (order.menuItems.find(item => {
      return item.productName.toString() == userRequest.productName.toString()
    })) {
      itemExist = true;
    }

    if (itemExist) {
      order.orderedItems.push({
        productName : userRequest.productName,
        quantity : userRequest.quantity
      })
      var cnt = 0;

      order.orderedItems.forEach( item => {
        cnt = cnt + item.quantity;
      })
      order.itemCount = cnt;
      console.log("item exists")
      throw fail.checkedError("Add success. show cart", "GoShowCart", {resultOrder:order});
    } else {
      order.errorMessage = '말씀하신 상품은 없네요'
      console.log("item NOT exists")
      throw fail.checkedError("Add failed. show menu", "GoShowMenu", {resultOrder:order});
    }
  }
  // should not be here
  return order;
}

메뉴 리스트에서 유저가 요청한 메뉴명이 있는지 찾아본 뒤에 있으면 카트에 추가한 뒤에 카트 보여주기 액션(ShowCart)으로 보내고, 없으면 에러 메시지를 넣은 다음에, 메뉴 보여주기 액션(ShowMenu)으로 보냅니다.

* 이 js 코드는 특이하게도, return을 하지 않습니다. 항상 checkedError를 통해서 익셉션으로 처리하게 하고 있습니다.

 

이때, throw fail.checkedError 를 사용합니다. 파라미터중에서, 첫번째는 로그에 나오게될 메시지이고,

2번째는 exception이름인데, 앞에 보여드렸던 AddItem 모델코드에 throw 블럭에 정의한 이름입니다.

3번째 파라미터는, exception을 던질때, 같이 보내줄 데이터들을 json 형태로 보냅니다. order를 resultOrder라는 이름으로 전달했고, 이 역시 모델링 코드에 동일한 이름으로 정의가 되어 있어야 합니다.

 

* 3번째 파라미터는 json 형태로, 1개의 컨셉만 보낼수 있는건 아니고, 2개 이상도 보낼 수 있습니다.

  { resultOrder:order, myItem:item} 처럼 쓰시면 됩니다.

 

resultOrder로 이름을 바꾼 이유는 모델링 코드에서는 이미 order 라는 이름이 인풋으로 있기때문에 겹치지 않도록 바꾸었습니다.

 


 

실행을 해보면,

 

"메뉴 보여줘" 라는 발화를 하면, ShowMenu가 시작되고, 메뉴 리스트를 보여줍니다.

 

딸기 우유 1개를 추가했더니, 카트에 담기고, 카트화면이 나옵니다.

 

"메뉴 보여줘" 발화를 해서 다시 메뉴 화면으로 이동합니다. 카트에 아이템은 그대로 유지되고 있습니다.

 

이번엔 메뉴 리스트에 없는 "초코 사탕"을 추가하려고 해보았습니다만, 메뉴에 없어서, 에러 메시지를 보여주고, 카트가 아닌 메뉴 화면으로 되돌아 갔습니다.

 

 

이번엔 메뉴에 있는 걸로 다시 담아 보겠습니다. 카트에 잘 담기고, 정상 메시지도 보이고 있습니다.

 

 

 

전체 코드는 아래 위치에 있으니 참고하세요~

https://github.com/earthworm925/bixby/tree/mart-checkederror-replan

 

빅스비 캡슐 개발의 중요한 부분 중 하나가 학습(Training) 인데요.

 

특정 상황에 효율적인 트레이닝 한가지를 공유드리고자 합니다.

 

일단, 파라미터로 들어가는 타입 중에 name 타입과 enum이 있는데요, 아시다 시피 둘다 vocab 을 가질 수 있습니다.

 

enum은 특히, exact matching 일때 해당 컨셉이 태깅 가능하죠. 즉 vocab에 없는 말은 컨셉에 안잡힙니다.
그리고, enum은 symbol 갯수가 500개 제한으로 알고 있습니다. 많이 넣지는 못하는것입니다.

name 타입은 기본적으로 vocab이 없을수도 있고, 있을 수도 있습니다.
vocab 갯수도 제한은 없고, 캡슐 전체 vocab 제한인 5만개 한도 내에서 넣을 수 있는것으로 알고 있습니다.


이번 포스트에서는 name 타입을 학습 할 때 가능한 팁에 대한 내용입니다.

 

vocab 이 없이 학습을 하면, training 데이터에서 태깅 한 위치의 앞뒤 문맥?(또는 단어들?)을 보고 해당 컨셉인지 판단을 하게 됩니다.

 

예를 들어 아래와 같은 트레이닝이 있다고 했을때,

"OOO 에서 배달 시켜줘"

OOO 의 위치에 RestaurantName 이라는 컨셉으로 태깅 했다면, "~에서 배달 시켜줘" 라는 말 앞에 나오는 단어들은 RestaurantName 컨셉이다! 라고 학습을 시켜주는 것입니다.

그래서 꼭 vocab이 없이도 컨셉을 잡아 낼 수 있습니다.

 

다만, 앞뒤에 오는 단어들의 변화, 동사어미의 변화등으로 인해서 완벽하게 잡아내지는 못합니다.

* 해보신분들은 잘 아시겠죠

 


그런데, 내가 개발하는 캡슐에서는 가급적 해당 컨셉을 잘 잡아내야 겠고, 갯수는 적지않이 많고 (enum을 쓰기에는 부족한?) 대신 무한하지는 않는 경우라면??

 

이럴때는 vocab에 해당 단어들을 넣은 뒤에, 학습을 하실 때에, 항상 vocab에 있는 것들만 학습에 꼭 넣어주시면 됩니다.

그러면, enum처럼 exact 매칭 처럼, vocab에 있는 것만 매칭되게 할 수 있고, 해당 컨셉 앞뒤의 variation으로 인한 태깅 실패 확률이 확 줄어 듭니다.

--> 빅스비는, '아 이 컨셉은 vocab에 있는 것들만 인식하면 되는구나' 라고 학습합니다.

 

예를 들어, 내 캡슐은 편의점 캡슐인데, 거기에 판매되는 메뉴의 종류가 수백개에서 천개 안팎이라고 했을 때 이 방법을 쓰는 것도 검토해볼만 합니다.

 

그리고,

 

여기에서 더 나아가서, 판매하는 메뉴들 이름이 일부분이 조금씩 중복되는 경우가 많다면 한 가지 더 좋은 방법이 있습니다.

 

예를 들면

 

딸기 우유, 초코 우유, 커피 우유, 딸기 아이스크림, 초코 아이스크림, 커피 아이스크림, 딸기 사탕, 초코 사탕, 커피 사탕 ...

 

뭔가 느낌이 오셧을 텐데요. 딸기/초코/커피/우유/아이스크림/사탕 6개의 단어들로만 메뉴명이 이루어져 있지만,

vocab은 3x3 = 9개가 필요한 상황입니다. 갯수가 늘어날 수록, 이름이 길어질 수록, vocab의 수는 확 늘어납니다.

 

 

이럴 땐, 단어 쪼개기를 하시면 됩니다.

 

 

ProductName.vocab 에 아래와 같이, 단어들을 공백 기준으로 쪼개어서 넣습니다.


vocab (ProductName) {
  "딸기"
  "커피"
  "초코"
  "우유"
  "아이스크림"
  "사탕"
}

* 쪼개는 기준은 표준어 기준으로 하시거나, 빅스비에게 말을 해서 인식 하는 띄어쓰기대로 쪼개셔도 됩니다.

 

그리고, 학습 하실 때, ProductName을 태깅할 때, 아래처럼 "ProductName + ProductName" = ProductName 이 되도록 학습 시키면 됩니다.

 

 

vocab에 있는 각각의 단어를 공백을 사이에 두고 연속으로 있을 떄, 1개의 ProductName 이 된다 라고 빅스비에게 학습을 시키는 것 입니다. 한단어도 물론 가능하고, 세단어 이상 연속도 가능합니다. 물론 길이도 다양하게 학습 시켜야합니다.

* 위 예시는 prompt 상황이긴 합니다만, root/continue 발화에서도 동일하게 적용 가능합니다.

 

다만 유의해야 할 점은, 실제로는 없는 메뉴명도 태깅 가능하기 때문에, 그런 것들은 로직(.js)에서 예외 처리를 해줘야 합니다.

즉, "커피 딸기" 같은 단어도 ProductName으로 인식 되기 때문이죠. 이럴땐, exception을 던져서, "없는 메뉴입니다" 라고 피드백 처리하는게 좋겠습니다.

 

 

실행을 해보면,

 

"주문할게요"

 

이번 예시에서는, candidate list 를 보여주지는 않았습니다. 조합이 여러개 일 경우 다 보여주기는 어렵기 때문이죠.

 

트레이닝에는 없었던 "초코 아이스크림"을 말했더니 잘 인식했네요. 디버그창에서도 확인해보겠습니다.

 

 

예외처리가 필요할 "커피 딸기" 도 해보면 인식은 됩니다.

 

js 로직에서 실제 메뉴 리스트에서, 발화로 인식된 메뉴명이 있는 지 찾는 로직도 구현이 필요할 것입니다.

그리고, 재고가 없거나, 실재하지 않는 메뉴명일때 예외처리도 필수겠지요.

 

여기까지 name type 단어 학습 팁, 단어 쪼개기에 대해서 알아보았습니다. 참고 코드는 아래 위치를 참고해주세요.

https://github.com/earthworm925/bixby/tree/mart-divided-vocab

 

첫번째 주제는 prompt 입니다.

 

대화형 AI 플랫폼이다 보니, 아무래도 prompt를 자주 쓸 경우가 생깁니다.

 

* Prompt 관련한 가이드 : https://bixbydevelopers.com/dev/docs/dev-guide/developers/enhancing-UX.prompts

 

Bixby Developers

The intelligent assistant platform built from the ground up for developers. Come join the Bixby Developer Program.

bixbydevelopers.com

특정 액션이 수행되는데, 필요한 정보 (파라미터, 컨셉)가 필요할 때, 유저에게 되묻는 것이지요

* 갑자기 액션이니, 컨셉, 프롬프트 이러한 말들이 생소하다면, 빅스비 개발자 사이트에서 기본 내용을 익히고 보시는 것이 좋을듯 합니다 

 

Primitive type 컨셉은 text, enum, integer, boolean 등등 여러가지가 있겠습니다.

 

이런 primitive 타입의 input prompt는 명확한 편입니다.. text가 필요하면 단순히 인풋창 하나 뜨고 입력하면 끝인데요.

예를 들어, 과일가게에서 주문을 한다고 했을때,

 

주인 : 무엇을 사시겠어요?
손님 : 사과
주인 : 몇개 드릴까요?
손님 : 5개

 

이런식으로 자연스럽게 주고 받을 수 있겠죠.

'사과'를 입력받을 수 있는 text (또는 name) 타입 한번, 갯수를 입력받을 수 있는 Integer 타입 한번씩 받으면 되겠습니다.

 

그런데, 이런 상황일 경우에는...

 

주인 : 무엇을 사시겠어요?
손님 : 사과 5개 주세여~

 

 

해보신분들은 아시겠지만, 이렇게 한번에 2가지 컨셉을 말해버리면, 예상치 못한 결과가 될 수 있겠습니다.

저 상태가 text 인풋 프롬프트 상황이라면, `사과` 만 받아들이고, 5 는 무시가 될 수도 있을듯 하네요.

그러고, 또 손님에게 몇개 살껀지 다시 물어보게되는, 좀 덜 똑똑해 보일수도 있겠습니다.

 

이럴때는 primitive type을 인풋으로 받는거 보다, structure 타입을 받도록 해서 한번이 2가지 컨셉을 말해도 다 알아듣도록 하는 것도 검토해 볼 수 있습니다.

 

사과와 같은 상품명은 ProductName 이라고 하고, 갯수는 Quantity 라고 명명하겠습니다.

이 둘을 한번에 받게 새로운 Structure type인 UserRequest 라는 것을 정의해 보겠습니다.

structure (UserRequest) {
  property (productName) {
    type (ProductName)
    min (Required) max (One)
    visibility (Private)
  }
  property (quantity) {
    type (Quantity)
    min (Optional) max (One)
    visibility (Private)
  }
}

* 저는 보통, visibility (Private)을 거의 항상 붙여둡니다. Public으로 되어 있으면, 멤버 property가 임의로 다른 액션의 인풋 등으로 자동으로 빨려 들어가는 경우가 있습니다. 대부분 원하지 않는 동작일 경우 많기 때문에 private 으로 해놓습니다.

 

structure를 만들었으니, prompt를 할 때 이 structure에 대해 prompt가 뜨도록 하면 됩니다.

주문 시작하는 액션을 StartOrder라고 해보겠습니다.

action (StartOrder) {
  type (Search)
  collect {
    input (userRequest) {
      type (UserRequest)
      min (Required) max (One)
      default-init {
        intent {
          goal : GetUserRequestCandidate
        }
      }
    }
  }
  output (Order)
}

* 판매 가능한 아이템 목록(candidates)을 가져오는 액션 GetUserRequestCandidate을 만들고 이 액션을 통해서 아이템들을 UserRequest 배열로 리턴해 줍니다.

 

UserRequest 컨셉의 인풋 프롬프트 상황에서, 유저가 ProductName 과 Quantity를 말할때, 그 2개의 데이터로 UserRequest를 만들 수 있어야 하므로, Constructor 액션을 만들겠습니다.

 

action (BuildUserRequest) {
  type (Constructor)
  collect {
    input (productName) {
      type (ProductName)
      min (Required) max (One)
      default-select {
        with-rule {
          select-first
        }
      }
    }
    input (quantity) {
      type (Quantity)
      min (Optional) max (One)
      default-select {
        with-rule {
          select-first
        }
      }
    }
  }
  output (UserRequest) {
    evaluate {
      UserRequest {
        productName : $expr(productName)
        quantity : $expr(quantity)
      }
    }
  }
}

evaluate로 간단히 아웃풋을 만들어낼 수 있어서, local-endpoint js 파일은 필요가 없습니다. 기타 예외처리 등의 전처리가 필요하면 js 파일에서 아웃풋을 만드셔도 됩니다. 복잡한 캡슐일수록 예외처리는 많아지죠.


그럼, 이제 인풋 프롬프트 상황을 학습해야합니다.

 

UserRequest의 prompt 이고, 인풋으로 ProductName 과 Quantity 를 태깅했더니, Constructor 함수인 BuildUserRequest가 중간에 불리고 있습니다.

* 혹시 UserRequest를 아웃풋으로 내면서, ProductName, Quantity를 입력으로 받는 액션이 또 있다면 충돌이 생길 수 있으므로, Route를 명시해야 될 때도 있습니다.

 

갯수는 (Quantity) 말을 안할 수도 있으니, optional로 했습니다. 그리고 Constructor에서 quantity가 undefined 이면, 그냥 1로 초기화 하게 했습니다. (또는 Quantity를 다시 유저에게 되묻게도 할 수 있습니다!)

 

 

기타 input-view 와 result-view도 적당히 작성을 하고 실행을 해봅니다.

 

 

"주문할게요"

 

 

수박 2개

 

 

실제로 '수박' '2' 를 각각 다 인식 했는지 debug 창에서 확인해보면, 실제로 잘 들어와 있음을 알 수 있습니다.

 

* 유저의 주문은 Order라는 컨셉 내부에 멤버 컨셉으로 가지고 있도록 했습니다. 일종의 카트 라고 생각하시면 될듯합니다.

 

여기 까지, 프롬프트 상황에서 한번에 여러가지 컨셉을(structure로 묶어서) 입력 받도록 하는 방법에 대해 알아보았습니다. 소스 코드는 아래 git을 참고하시면 됩니다.

https://github.com/earthworm925/bixby/tree/mart-multi-input-prompt

 

 

 

+ 추가로, 수량을 말 안했을때 다시 수량만 되묻게 하고 싶다면, BuildUserRequest 액션의 Quantity 인풋을 required 바꾸시고, Quantity에 대한 프롬프트 학습도 추가하시면 됩니다.

 

"무엇을 사시겠어요?"
"수박"
"몇개 사시겠어요?"
"하나"

 

위의 git 소스에는 이렇게 반영되어 있습니다.

'Bixby 개발' 카테고리의 다른 글

fail.checkedError 와 Replan 활용  (0) 2019.12.13
효율적인 학습 방법 - 단어(vocab) 쪼개기  (0) 2019.12.10
Bixby 캡슐 개발하기  (0) 2019.11.07

이곳에 삼성의 대화형 AI 플랫폼 빅스비 캡슐 개발 노하우를 공유하고자 합니다.

 

개발툴인 빅스비 스튜디오 설치 및 기본 개발관련 정보는 아래 사이트에서 확인 할 수가 있습니다~

 

https://bixbydevelopers.com

 

Bixby Developers

The intelligent assistant platform built from the ground up for developers. Come join the Bixby Developer Program.

bixbydevelopers.com

기본적인 문법이나 샘플 캡슐 정보들도 위 사이트에서 다 볼 수 있습니다~

(또는 다른 개인 블로그등에서도 소개하는 내용들이 있더군요)

 

저는, 개인적으로 공부해보면서 배운 경험, 노하우 (특히 여러 Work around ^^) 등을 이곳에 공유해 볼까 합니다.

 

Bixby의 공식 가이드가 아닌 개인적인 경험이므로, 공식 가이드와 일부 내용이 다를수도, 틀린 부분이 있을수도 있습니다

 

IDE가 잘 되어 있어서 개발이 매우 쉬운 것 같습니다. 실행화면도 바로 볼 수 있고, 디버깅도 가능합니다.

 

빅스비 자체 모델링 언어와 Javascript 를 활용하면, 생각보다 다양한 시나리오가 구현이 가능하더군요.

 

공유할 내용들은 빅스비 개발 경험이 있는 분들께 도움이 되었으면 하는 내용들이어서, 기본적인 내용은 따로 다루지 않고 있습니다. 내용이 조금 어려울 수도 있습니다 :)

+ Recent posts