Clovaで「日本酒診断」のスキルを作ってみる(1)


はじめに

前回作った抵抗値換算スキルが無事に公開できたということで、今回はまた別のスキルに挑戦してみました。最近私自身が日本酒にはまっているということで「日本酒診断」というスキルを開発していこうと思います。ちなみに私は谷川岳が好きです。

スキルについて

Clovaがユーザにいくつかの質問をして、そのユーザの好みにあった日本酒を提案するスキルです。一応事前に伝えておきますが、これは完全なるネタスキルです

日本酒の提案モデルについて

下の図のような会話のフローで日本酒を提案します。

こんな感じで会話を分岐させてユーザにあった日本酒を提案しようと思っています。まあ、ちょっと主観が入っちゃって谷川岳が多めになってしまいましたね(^^;

技術的に挑戦すること

今回は以下の3つの項目で挑戦しようと思っています。

  • TypeScriptで書く
  • 公式のSDKを使う
  • 前回セッションのスロットを記憶する

TypeScriptで書く方法はこちらを参考にしたりなどをしました。とりあえずスタートラインとして、TypeScriptのための環境構築、実装としては「インテントを受け取りそれを返事として返す」ところまでをやりました。また公式SDKを使う場合は前回少し触れたように、少し厄介なことがあったのでそれの対処を行いました。

TypeScriptの環境構築

今回もFirebaseを用いたので、そのままfirebase initを適当なディレクトリで実行した後に使用言語にTypeScriptを選んだだけです。ベーシックな依存関係とか云々はFirebaseが用意してくれました。しかしfunctions/tsconfig.jsonに関しては少しだけ変更を加えました。

functions/tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "noImplicitReturns": true,
    "noUnusedLocals": true,
    "outDir": "lib",
    "sourceMap": true,
    "strict": true,
    "esModuleInterop": true,
    "target": "es2017"
  },
  "compileOnSave": true,
  "include": [
    "src"
  ]
}

変更点としては"esModuleInterop": trueを加えました。理由はこの後にinstallする@types/expressのimport/exportに関する記述の部分でtscのerror発生を防ぐためです。
続けて以下の3つのパッケージを追加でインストールしました。

  • @line/clova-cek-sdk-nodejs
  • @types/express
  • express

TypeScript(以下TS)でexpressを使う場合は@types/expressも必要みたいです。

TypeScriptでの実装

こんな感じで実装しました。TS初心者なので色々ツッコミどころあるかもなのでコメントくれると嬉しいです。

functions/index.ts
import * as functions from 'firebase-functions';
import Express from 'express';
import * as Clova from '@line/clova-cek-sdk-nodejs';

const extensionId : string = functions.config().clova.extension.id;

const clovaSkillHandler = Clova.Client
    .configureSkill()

    //起動時に喋る
    .onLaunchRequest((responseHelper: { setSimpleSpeech: (arg0: { lang: string; type: string; value: string; }) => void; }) => {
        responseHelper.setSimpleSpeech({
            lang: 'ja',
            type: 'PlainText',
            value: `日本酒診断をします。いくつかの質問に答えてください。`,
        });
    })

    //ユーザーからの発話が来たら反応する箇所
    .onIntentRequest(async (responseHelper: { 
        getIntentName: () => string; getSlots: () => string; setSimpleSpeech: { (arg0: { lang: string; type: string; value: string; }): void; (arg0: Clova.Clova.SpeechInfoText, arg1: boolean): void; }; }) => {
        const intent = responseHelper.getIntentName();
        //const slots = responseHelper.getSlots();

        console.log('Intent:' + intent);

        let speech = {
            lang: 'ja',
            type: 'PlainText',
            value:  `${intent}のインテントを受け取りました`
        }

        responseHelper.setSimpleSpeech(speech);
        responseHelper.setSimpleSpeech(
            Clova.SpeechBuilder.createSpeechText('開発中です'), true
        );
    })

    //終了時
    .onSessionEndedRequest((responseHelper: { getSessionId: () => void; }) => {
        //const sessionId = responseHelper.getSessionId();
    })
    .handle();

const app = Express();

const clovaMiddleware = Clova.Middleware({applicationId: extensionId});
app.use( function (req, res, next) {
    req.body = JSON.stringify(req.body)
    next()
})
app.post('/clova', clovaMiddleware, <Express.RequestHandler>clovaSkillHandler);

exports.clova = functions.https.onRequest(app);

実のところほとんどJavaScript(以下JS)の実装のコピペでなんとかなった感じです。resonseHelperのところはJSでは一々オブジェクトの中のフィールドや型を指定する必要がなかったのですがTSではそれが許されませんでした。でもこの辺りはvscodeの機能が勝手に対応してくれました(やっぱりvscodeは便利)。後はapp.post()のところでclovaSkillHandlerを指定するときに、clovaSkillHandlerのメソッドチェーンの最後にhandle()があるわけですが、このままだとclovaSkillHandlerはただのFunctionなので<Express.RequestHandler>を付けてRequestHandlerとして指定する必要があります。

公式SDKとFirebaseの相性の悪さ

この記事でも述べられている通りRequestがオブジェクトで自動で返ってくるせいでClova.Middlewareが実行したときに文字列が受け取れないことからerrorを起こしてしまいます。先ほどの記事ではSDKのソースをご自身で書き換えたみたいですが、できれば公式のSDK使いたいと思ったので(実はTSを選んだのも公式SDKがTSで実装されているから)、ミドルウェアがリクエストボディを受け取る前に

app.use( function (req, res, next) {
    req.body = JSON.stringify(req.body)
    next()
})

を加えてreq.bodyをstringに変換させる処理を加えてやりました。

いまのところまでの進捗

事前に「冷/熱燗」というスロットを含んだhowDrinkIntentを用意してそれを正しく受け取れたかのテストを行いました。まあこのくらいの内容はJSだったら一瞬だったと思いますが、TSで苦労して書いたことでまた別の感動がありますね(笑)

さいごに

とりあえずベースができたので次は分岐する会話のモデルの実装をしていこうと思います。session.sessionAttributesというCEK APIのMessage fieldを使うと前回のセッションの内容の受け渡しができるみたいなので、これでやっていこうと思います。今回までの分もGitHubにコミットしておきました。