原文链接:https://blog.techbridge.cc/2016/07/02/ChatBot-with-Wit/

今天我们要让我们的Chat Bot 更加聪明,利用被Facebook 收购的Wit.ai 所提供之API,可以很方便的让Chat Bot 有了NLP 的支援,让他/她更加聪明!
实际上Wit.ai 的介面并没有我想像中的好用,需要很有耐心地把官网上的教学一步步做完,并且了解他所定义的名词代表之含义,虽然写得很详细,但毕竟是英文,因此就记录一下整个过程,并跟大家分享。

Step 1 注册Wit.ai 帐号

先到Wit.ai的官方网站注册一个帐号,有GitHub与Facebook可以选择。
[转载]利用 Wit.ai 讓你的 Messenger Bot 更聰明! - 图1

Step 2 Dashboard 设定

接着你会进到你的Dashboard
[转载]利用 Wit.ai 讓你的 Messenger Bot 更聰明! - 图2
点选右上角的+号,进入App设定页面,进行简单的设定,基本上只要设定名字与描述,语言等等之后还能修改。
这边要提一下,我本来想设定成Chinese,但后来在建立机器人对话故事时,发现他的中文支援好像还不是很完善,常常判断不出Entity,因此这边还是先以英文为例子,如果有高手知道怎么解的话也欢迎告诉我!
[转载]利用 Wit.ai 讓你的 Messenger Bot 更聰明! - 图3

Step 3 创建对话情境(Story)

继续,设定完后就进入到编辑界面,在Wit.ai里面,你的机器人与一般使用者的对话情境,都叫做Story,你可以透过创建Story来定义出在这个情境下,你的机器人要怎么跟使用者对话。
[转载]利用 Wit.ai 讓你的 Messenger Bot 更聰明! - 图4
[转载]利用 Wit.ai 讓你的 Messenger Bot 更聰明! - 图5
整个介面就像是一个对话视窗,看起来颇亲切,左边是User says,右边是Bot sends, Box executes与Jump。
先简单介绍,看完后面的例子会更清楚。

  • User says:顾名思义就是定义User会说的话,并且你可以设定User的句子中,有哪些关键字是你需要的、哪些文字是代表什么含意,在Wit.ai的世界中,这样的东西叫做Entity,后面会再度说明。
  • Bot sends:就是机器人要回覆的句子,这边可以带入一些参数,像是user所提及的一些关键字,或是机器人向外呼叫API所得到的结果。
  • Box executes:就是让你定义机器人要执行的function,真正的实作不会在这边,这边只是定义名称,以及要接收的context与吐回的变数名称。
  • Jump:则是让你能够在满足设定的一些条件之下,跳回到某个Box executes或是Box sends的步骤去执行。

    Step 4 定义使用者语句

    接下来我们先定义User可能会对我们的机器人说的话,像是使用者可能会跟机器人打招呼,我们就可以在对话框的User says输入Hello,并且highlight起来以后设定Entity,Enity在Wit.ai里面,就是用来让系统判断使用者输入句子时,哪些关键字是要抽取出来做处理的,你可以依照该关键字的特型来设定相对应的Entity类别。
    这边我们就自定义一个Entity名称叫做greeting,当然Wit.ai也有许多内建好的Entity,当你点选Add a new entity时,他会有提示。
    [转载]利用 Wit.ai 讓你的 Messenger Bot 更聰明! - 图6
    你可能会想说,打招呼又不会只说Hello,你这样设定的话,我照之前方法hardcode 写在server side 就好了呀,要Wit.ai 干麻。
    Wit.ai当然没有这么简单,介面上方的Tab是不是有个学士帽写着Understanding?在这个地方你有三种方式可以用来训练你的机器人:

  • 增加例句

在上方写着Test how your bot understands a sentence的地方输入更多的例句,并且如同前面步骤般去定义Entity,这边要注意的是,当你输入完例句后,记得点选下方绿色的Validate,让Wit.ai去记住你的例句。成功的话就会看到下方Entity的Values栏位会多出你刚刚例句中所抓取到的关键字(以下图例子来说就是Hi也被我们纳进greeting这个entity内了,只要之后user输入Hello或是Hi,都是属于greeting)
[转载]利用 Wit.ai 讓你的 Messenger Bot 更聰明! - 图7

  • 增加Entity的Keyword与Synonyms

你也可以点选下方的Entity名称,进去手动增加关键字或是同义词。
关键字同义词的关系有点像是父子类别,这边举个比较易懂的例子,如下图,我们有个Entity叫做Beer,底下的关键字是啤酒与红酒,当使用者喊出啤酒的时候,机器人就会知道是属于Beer这个Entity。
但啤酒有很多种种类,我们可以在同义词这边增加:蜂蜜啤酒,这样当使用者输入蜂蜜啤酒时,机器人就会判断目前的Beer Entity的Value为啤酒,而非红酒。
相同的,我们也可以设定葡萄酒为红酒的同义词,让使用者喊出葡萄酒时,机器人会判断为红酒。
要注意的是,机器人记住的Entity Value 会是以Keyword 为主,也就是你输入蜂蜜啤酒,但对机器人来说,侦测到的Beer Entity,其值为啤酒,而非蜂蜜啤酒。
[转载]利用 Wit.ai 讓你的 Messenger Bot 更聰明! - 图8

  • 设定Entity的Search strategy

最后在设定Entity 的地方还有Search strategy 可以选择,意思是你希望Wit.ai 要怎么样从句子中找出这个Entity 。

  1. trait:如果你想设定的Entity并不是由单一一个关键字就可以判断,也不是靠句子中几个关键字或是子句能够辨别,而是需要整个句子来判定的话,就要设定成trait,像是今天的例子里面,想要问新闻,这种使用者的意图就很适合设定成trait。

官网范例:出处
[转载]利用 Wit.ai 讓你的 Messenger Bot 更聰明! - 图9

  1. free-text: 如果你想要撷取使用者句子中的某段文字,而该段文字并不是特定的关键字时,就要设定free-text,像是User 说:“Tell Jordan that I will be home in ten minutes”,而你想要撷取”I will be home in ten miutes”,这时就可以把想要撷取的句子选取起来,设定为free-text,要注意的是,free-text 一定要搭配keywords 一起使用,有点像是告诉Wit.ai 说这段话都算是keywords,但不一定要exactly match 才能触发。

官网范例:出处
[转载]利用 Wit.ai 讓你的 Messenger Bot 更聰明! - 图10

  1. keywords: 要完全符合你预先设定的关键字才会触发。

官网范例:出处
[转载]利用 Wit.ai 讓你的 Messenger Bot 更聰明! - 图11

Step 5 定义机器人回覆语句

介绍了这么多琐碎的东西后,回过头来看看我们要怎么设定机器人的回覆。以最前面的例子来说,当机器人收到greeting的Entity后,可以让用相同的entity value回覆,并加上简单的介绍。
点选下方的Bot sends,对话框就会出现机器人的部分,你可以在里面输入机器人的回覆语句,想要的变数可以用{ }包起来,这边我们直接使用greeting这个entity,这样就能用同样的Entity去回覆。
[转载]利用 Wit.ai 讓你的 Messenger Bot 更聰明! - 图12
画面右下方有个浮动的按钮“Press ~to chat with your bot”,可以让你即时测试一下。
[转载]利用 Wit.ai 讓你的 Messenger Bot 更聰明! - 图13

Step 6 设定机器人执行动作

当然机器人不能只单纯回话,要能够执行动作,这边我们创建另一个对话情境,设定让我们的机器人帮忙找新闻!
这边我先设定好一个使用者语句与相关Entity,接着先让Bot executes动作,也就是让他执行一个Funtion,这边只是定义Function名称以及输出的参数,实际的实作要等到后面撰写程式时才需要。
[转载]利用 Wit.ai 讓你的 Messenger Bot 更聰明! - 图14
从上图来看,我设定了一个getNews的函式,并且设定一个context为newsResult,代表这个function会有一个变数newsResult可以供外部与自己使用。此外,机器人会先回覆一个讯息,其中包含你的search_queryentity之value
设定完一样要进行一下测试,当你输入使用者语句后,机器人会执行函式,并问你要执行哪个Context,这时你就点选刚刚设定的newsResult当作回覆,教导机器人记住这个context
[转载]利用 Wit.ai 讓你的 Messenger Bot 更聰明! - 图15
若使用者没有说他想找什么新闻怎么办呢?这时候就是另用另一个Context来判断了!你可以设定一些context branch,透过先前提到的Jump,让机器人根据Context的不同来执行不同回覆。
透过定义一个missNews的Context,告诉机器人,当没有search_query时,可以怎么做。
如下图,你需要设定一个BookMark让你的机器人可以Jump到那个Context下。
[转载]利用 Wit.ai 讓你的 Messenger Bot 更聰明! - 图16
设定完后依然需要先测试一下,训练一下你的Bot。在这边你要告诉机器人目前是哪个Context 。要注意的是,你必须要把非当下必要的Context移除,像是下图中,在User回答Brexits后,需要把missNews这个context点选掉,这样Bot才会正常的跳回getNews
[转载]利用 Wit.ai 讓你的 Messenger Bot 更聰明! - 图17

Step 7 套用API 与实作Function

前面几个步骤做完后,就有个基本的使用者与机器人互动情境,接下来就可以开始实作函式,并套用API了。
这边以Node.js为例子,你需要先到你的专案底下加入node-wit这个package。

1 npm install —save node-wit

之后可以先测试一下,修改官方的example/quickstart.js,实作出getNews函式,这边先简单echo一下就好。程式码短短的,你需要注意的是actions这个object,里面定义了Bot要执行的动作函式,send是用来让Bot回话的,这一定要有,而我们自己定义的getNews就定义在下方。
getNews里面利用firstEntityValue从接收到的entities中找出你要的,这边我们要的当然是search_query的值。接着就可以去进行需要的处理,呼叫API等等。
唯一要注意的就是这边需要使用Promise 回传喔!

| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | const actions = { send(request, response) { const {sessionId, context, entities} = request; const {text, quickreplies} = response; return new Promise ( function ( resolve, reject ) { console .log( ‘sending.. .’ , JSON .stringify(response)); return resolve(); }); }, getNews({context, entities}) { return new Promise ( function ( resolve, reject ) { var

  1. search_query = firstEntityValue(entities, 'search_query' ) **if** (search_query) { context.newsResult = search_query + '最近很多人讨论...' ; _// we should call a real API here_ } **else** { _// To-do_ } **return** resolve(context); }); }, };

| | —-: | :—- |

执行node example/quickstart.js <Wit.ai server-side Token>
就会得到以下结果。
[转载]利用 Wit.ai 讓你的 Messenger Bot 更聰明! - 图18
在这边先打岔一下,我们回到Wit.ai的Dashboard看一下,会发现Inbox上面有个小红点?
Wit.ai会在这个地方纪录User传送进来的句子,并且让你在这边操作它,也就是说,你可以在这边利用User传入的句子来training你的机器人!让他直接从使用者身上学习!我觉得很棒的一个功能!
[转载]利用 Wit.ai 讓你的 Messenger Bot 更聰明! - 图19
ok,镜头再转回到程式码。
已经知道怎么实作函式了,那就接着把他跟Messenger api 结合吧!
其实跟刚刚的quickstart.js 比较不一样的的地方在于,你必须记录起来每一个fb user 的session,这样Wit.ai Bot 才会知道要回传给哪个FB user。

| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // This will contain all user sessions. // Each session has an entry: // sessionId -> {fbid: facebookUserId, context: sessionState} const sessions = {};const findOrCreateSession = ( fbid ) => { let sessionId; // Let’s see if we already have a session for the user fbid _Object .keys(sessions).forEach( k => { if (sessions[k].fbid === fbid) { // Yep, got it! sessionId = k; } }); if (!sessionId) { // No session found for user fbid, let’s create a new one_ sessionId = new Date ().toISOString(); sessions [sessionId] = { fbid : fbid, context : {}}; } return

sessionId; }; | | —-: | :—- |

接着我们其实就只要修改先前的quickstart.js以及先前实作过的messenger API的部分code即可。
因为我们的使用情境会让Bot在接收讯息时,立刻先回传文字,接着才会回传查询结果,而查询结果则需要利用Messenger API传送GenericMessage的结果,因此会需要两种return Method。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 5758 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 const firstEntityValue = ( entities, entity ) => { const val = entities && entities[entity] && Array .isArray(entities[entity]) && entities[entity].length > 0 && entities[entity][ 0 ].value; if (!val) { return null ; } return typeof val === ‘object’ ? val.value : val; }; // Our bot actions const actions = {true // Wit.ai的action中,一定要实作的send method,用来让机器人说话 send(request, response) { const {sessionId, context, entities} = request; const {text, quickreplies} = response; // find out user id const recipientId = sessions[sessionId].fbid; if (recipientId) { //这边需要判断要回传的讯息是否为查询结果__//若context中带有newsResult那就是要回传查询结果//因此就要呼叫sendNewsMessagePromise()来回传GenericMessage if (context.newsResult) { // fbBotUtil.sendNewsMessagePromise这边是Messenger API的相关实作return fbBotUtil.sendNewsMessagePromise(recipientId, context.newsResult) .then( () => null ) .catch( ( err ) => { console .error( ‘Oops! An error occurred while forwarding the response to’ , recipientId, ‘:’ , err.stack || err ); }); } else { //直接回传普通文字return fbBotUtil.sendTextMessagePromise(recipientId, text) .then( () => null ) .catch( ( err ) => { console .error( ‘Oops! An error occurred while forwarding the response to’ , recipientId, ‘:’ , err.stack || err ); }); } } else { console .error( ‘Oops! Couldn\‘t find user for session:’ , sessionId); // Giving the wheel back to our bot return Promise .resolve() } }, //我们自定义的getNews action getNews({context, entities}) { return new Promise ( function ( resolve, reject ) { var searchquery = firstEntityValue(entities, ‘search_query’ ) if (search_query) { //这边是去呼叫api _// fetchr是我实作的一个小函式,利用import.io去抓Yahoo news的搜寻结果。//不是这篇重点我就先略过啦~ fetchr(search_query, function ( data ) { context.newsResult = data; console .log( ‘context newsResult’ , context.newsResult); delete context.missNews; }); } else { context.missNews = true ; delete context.newsResult; } return resolve(context); }); }, };

实作完Actions 的部分,记得到router 里面去增加Wit.ai 的相关Setting

1 2 3 4 const wit = new Wit({ accessToken: , actions, });
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 router.post( ‘/‘ , function ( req, res ) { messagingevents = req.body.entry[ 0 ].messaging; for (i = 0 ; i < messaging_events.length; i++) { event = req.body.entry [ 0 ].messaging[i]; sender = event.sender.id; var sessionId = findOrCreateSession(sender); if (event.message && event.message.text) { text = event.message.text; // Handle a text message from this sender wit.runActions( sessionId, // the user’s current session text,// the user’s message sessions[sessionId].context // the user’s current session state ).then( ( context ) => { // Our bot did everything it has to do. // Now it’s waiting for further messages to proceed. console .log( ‘Waiting for next user messages’ ); // Updating the user’s current session state_ sessions[sessionId].context = context; }) .catch( ( err ) => { console .error( ‘Oops! Got an error from Wit: ‘ , err.stack || err); }) } } res.sendStatus( 200 ); });

上面这大串code其实就是接收到你在Messenger POST出去的讯息后,呼叫定义好的wit.runActions,然后就可以让Wit.ai帮你分析User的语句,并且回覆出去。
最后这边放一下送出我这边用到的fbBotUtil.sendNewsMessagePromise,也就是送出messenger GenericMessage 的程式码
Messenger GenericMessage API Usage

Final Result

刚刚我们设定的语句,是不是就透过Messenger 送出来了呢~
[转载]利用 Wit.ai 讓你的 Messenger Bot 更聰明! - 图20

One more thing

最后介绍一个方便的工具,ngrok。
ngrok 可以让你把localhost 转成外网可以存取的网址,也支援https,因此我们Debug 就方便多了,不需要每次都把程式Deploy 到远端机器以后才能测试,log 也能直接在本机端终端机看到!
他的设定超简单,到https://ngrok.com/download把程式下载回来,并且执行./ngrok http PORT
会出现如下画面,连https 的网址都有!这样一来,facebook要求的https webhook 就不成问题了,当然实际上运行还是要去用SSL 凭证啦…
[转载]利用 Wit.ai 讓你的 Messenger Bot 更聰明! - 图21
参考资料