2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Voiceflowで作成したアシスタントをAlexaスキルとして実行する(2024.5現在)

Posted at

Voiceflowの仕様が2023年11月に変更となり、これまで可能だったVoiceflowからAlexaスキルの直接作成ができなくなりました。仕様変更による利点もあるのですが、Alexaスキルの簡便な開発ツールとしてVoiceflowを利用していた人には大きな混乱をもたらしたようです。
この記事ではとにかくVoiceflowでAlexaスキルを作成したいという方向けに、それを実現する方法を紹介します。

1.Voiceflowの仕様変更
 2023年秋の仕様変更まではVoiceflowのデザイン画面の右上にPublishボタンがあり、それをクリックするとエラーがなければamazonの alexa developer consoleが開いてalexa環境上で動作確認ができました。
image.png
  ところが仕様変更後にあらたにassistantを作成するとalexa関連の初期設定がなくなっている上に、デザイン画面でもPublishボタンの配置が変更になり、かつ、このボタンをクリックしてもAlexaとは連携されません。
image.png

  この変化は新規にassistantを作成した場合に現れるので、仕様変更前に作成したassistant をテンプレートとして使い回しているときには現れません。結果的に、2023年11月以降にVoiceflowを使い始めたユーザがこの問題に直面することとなります。
 この原因は、AIスピーカーのソフト開発ツール、すなわちGoogle AssistantやAlexa Skillの開発ツールとしてスタートしたVoiceflowが汎用性を高めて独立のツールを志向しはじめたことにあります。今まではVoiceflowをAlexa環境で使用する場合、外見的にはVoiceflow -> Alexaスキル直結だったのが、現在はAlexaのコードからいわば関数としてVoiceflowのassistantを呼び出すという仕様になっています。
  こうした仕様変更は、任意のソフトからVoiceflowを利用できるという意味での汎用性は高くなりましたが、Alexaスキルの面倒なコードを書きたくなくてVoiceflowを使い始めたユーザにとってはジレンマ以外の何物でもありません。

2.対応策の概要
 対応策としては、alexa developer consoleで新規にスキルを作成し、そのコードを編集して、Alexaが受けた音声をvoiceflowに渡し、戻りデータをAlexaで出力するという流れになります。一度テンプレートを作成してしまえば、Voiceflowのassistantに割り当てあられているAPIキーの値を書き換えるだけで、使い回しができます。
 難しそうですが、具体的なコードの意味はよくわからなくても汎用的なひな形があるので、それを用いれば心配は不要です。ただし従来以上に、VoiceflowとAlexaとでは同一内容なのに異なる処理となることがありますので、その点は注意が必要です。

3.Voiceflowでの準備
  Alexaスキルとして実行したいVoiceflowのAssistantを作成します。Assistant名として"testrun"、Modalityとして"Voice"、Langurageとして"Japanese"を設定し、「Continue」を押します。その次の画面はデフォルトのまま「Create Assistant」をクリックします。

image.png

デザイン画面で動作確認用に"Start"のほかには"speakブロック"一つだけのAssistantを作り、Publishボタンをクリックします。Version nameが問われますが空欄のままPublishをクリックします。

image.png

"Successfully Published"と表示されたら、「Dialog Manager API」をクリックします。

image.png

Alexaから呼び出すときには、デフォルトでマスキングで表示されるAPIキーが必要となります。「Copy API Key」を押してテキストエディタなどに保存しておきます。

image.png

Voiceflow側の準備はこれだけです。あくまで例題なので任意の内容のAssistantを準備してかまわないはずなのですが、いまはCaptureやChoise,Intentなどの使用は避けてください(後で説明するようにVoiceflowとAlexaで調整が必要です)。

4.Alexa developer consoleでの準備
4.1 新規スキルの作成
 まずはamazon developer(https://developer.amazon.com/ja/)にログインし、メニュー "Alexa"から ”Alexa skill 開発”をクリックしさらに"コンソール"をクリックします(alexa developer consoleにアクセスできればほかの方法でアクセスしてもかまいません)。
*英語表示の画面が現れた場合にはウィンドウ左下の言語ボタンで「Japanese(日本語)」を選択してください。日本語を選択しても一部表示は英語のままとなることがありますが、本文で説明する内容と対応する操作をすれば大丈夫です。
 Alexaスキル画面で「スキルの作成」をクリックします。

image.png
 作成するスキルの内容に応じてスキル名とプライマリーロケールを指定してください。例ではスキル名として「接続くん」、プライマリーロケールを日本語としています。
image.png
【次へ】をクリックして、次の画面で「1. エクスペリエンスのタイプ」:その他 を選択します。それ以外はデフォルトとします。
【次へ】をクリックして、テンプレート選択画面で「スクラッチで作成」を選びます。「スクラッチで作成」を指定しても実際にはHello Worldスキルのテンプレートが読み込まれます。
【次へ】をクリックして「選択内容の確認」をします。誤りがなければ【スキルを作成する】をクリックします。スキルが準備できるまでしばらく待ちます。
image.png

 スキルの準備ができたら、「呼び出し名」をクリックして、呼び出し名(「Alexa、○○を起動して」の○○部分)を指定します。

image.png
 この例では入力部のchange meとある部分を上書きして、「呼び出しくん」とします。そして「スキルをビルド」をクリックします。
*すでに呼び出し名が「呼び出しくん」というスキルを作成済みの場合には別の名前としてください。

image.png

4.2 コードの書き換え
 ここまでの操作で用意できたのは "Hello World"スキルなので、これをVoiceflowと接続可能な汎用スキルに書き換えます。メニューの「コードエディタ」を選択し、コードを表示させます。
 書き換えなければならない部分は多いのですが、Voiceflowで作成したAssistantを単にAlexaスキルとして実行するだけであれば、各部分を以下に示すコードと置換するだけですみます。
*具体的にどのような操作をしているか知りたい方は
https://xavidop.me/alexa/2023-11-21-voiceflow-alexa/
https://github.com/xavidop/voiceflow-alexa-integration/tree/main/lambda
を参照してください。ここで紹介しているコードはほとんど上記サイトからの引用です。
 
(1)index.jsの書き換え
 index.jsを選択・表示させ、以下のコードと置換します。一般的なテキストエディタの操作でできます。

image.png

index.js
/* *
 * This sample demonstrates handling intents from an Alexa skill using the Alexa Skills Kit SDK (v2).
 * Please visit https://alexa.design/cookbook for additional examples on implementing slots, dialog management,
 * session persistence, api calls, and more.
 * */
const Alexa = require('ask-sdk-core');
const utils = require('./util.js');


const LaunchRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
    },
    async handle(handlerInput) {

        let chatID = Alexa.getUserId(handlerInput.requestEnvelope).substring(0, 8);
        const messages = await utils.interact(chatID, {type: "launch"});

        return handlerInput.responseBuilder
            .speak(messages.join(" "))
            .reprompt(messages.join(" "))
            .getResponse();
    }
};

const ListenerIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
    },
    async handle(handlerInput) {

        let chatID = Alexa.getUserId(handlerInput.requestEnvelope).substring(0, 8);
        const intent = Alexa.getIntentName(handlerInput.requestEnvelope);
        const entitiesDetected = utils.alexaDetectedEntities(handlerInput.requestEnvelope);

        const request = { 
            type: "intent", 
            payload: { 
                intent: {
                    name: intent
                },
                entities: entitiesDetected
            }
        };

        const messages = await utils.interact(chatID, request);

        return handlerInput.responseBuilder
            .speak(messages.join(" "))
            .reprompt(messages.join(" "))
            .getResponse();
    }
};

const HelpIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.HelpIntent';
    },
    handle(handlerInput) {
        const speakOutput = 'You can say hello to me! How can I help?';

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};

const CancelAndStopIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && (Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.CancelIntent'
                || Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.StopIntent');
    },
    handle(handlerInput) {
        const speakOutput = 'Goodbye!';

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .getResponse();
    }
};
/* *
 * FallbackIntent triggers when a customer says something that doesn’t map to any intents in your skill
 * It must also be defined in the language model (if the locale supports it)
 * This handler can be safely added but will be ingnored in locales that do not support it yet 
 * */
const FallbackIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.FallbackIntent';
    },
    handle(handlerInput) {
        const speakOutput = 'Sorry, I don\'t know about that. Please try again.';

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};
/* *
 * SessionEndedRequest notifies that a session was ended. This handler will be triggered when a currently open 
 * session is closed for one of the following reasons: 1) The user says "exit" or "quit". 2) The user does not 
 * respond or says something that does not match an intent defined in your voice model. 3) An error occurs 
 * */
const SessionEndedRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'SessionEndedRequest';
    },
    handle(handlerInput) {
        console.log(`~~~~ Session ended: ${JSON.stringify(handlerInput.requestEnvelope)}`);
        // Any cleanup logic goes here.
        return handlerInput.responseBuilder.getResponse(); // notice we send an empty response
    }
};
/* *
 * The intent reflector is used for interaction model testing and debugging.
 * It will simply repeat the intent the user said. You can create custom handlers for your intents 
 * by defining them above, then also adding them to the request handler chain below 
 * */
const IntentReflectorHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest';
    },
    handle(handlerInput) {
        const intentName = Alexa.getIntentName(handlerInput.requestEnvelope);
        const speakOutput = `You just triggered ${intentName}`;

        return handlerInput.responseBuilder
            .speak(speakOutput)
            //.reprompt('add a reprompt if you want to keep the session open for the user to respond')
            .getResponse();
    }
};
/**
 * Generic error handling to capture any syntax or routing errors. If you receive an error
 * stating the request handler chain is not found, you have not implemented a handler for
 * the intent being invoked or included it in the skill builder below 
 * */
const ErrorHandler = {
    canHandle() {
        return true;
    },
    handle(handlerInput, error) {
        const speakOutput = 'Sorry, I had trouble doing what you asked. Please try again.';
        console.log(`~~~~ Error handled: ${JSON.stringify(error)}`);

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};

/**
 * This handler acts as the entry point for your skill, routing all request and response
 * payloads to the handlers above. Make sure any new handlers or interceptors you've
 * defined are included below. The order matters - they're processed top to bottom 
 * */
exports.handler = Alexa.SkillBuilders.custom()
    .addRequestHandlers(
        LaunchRequestHandler,
        ListenerIntentHandler,
        HelpIntentHandler,
        CancelAndStopIntentHandler,
        FallbackIntentHandler,
        SessionEndedRequestHandler,
        IntentReflectorHandler)
    .addErrorHandlers(
        ErrorHandler)
    .withCustomUserAgent('sample/hello-world/v1.2')
    .lambda();

(2)util.jsの書き換え
 util.jsを選択・表示させ、以下のコードと置換します。ただし提示したコードの2行目にある "MY VF API KEY"は引用符の中身をVoiceflowのAssistant作成時に取得したAPIキーと置換してください。引用符「"」は残します。

image.png

util.js
const axios = require('axios');

const VF_API_KEY = "MY VF API KEY";

module.exports.interact = async function interact(chatID, request) {
    let messages = [];
    console.log(`request: `+JSON.stringify(request));

    const response = await axios({
        method: "POST",
        url: `https://general-runtime.voiceflow.com/state/user/${chatID}/interact`,
        headers: {
            Authorization: VF_API_KEY
        },
        data: {
            request
        }

    });

    for (const trace of response.data) {
        switch (trace.type) {
            case "text":
            case "speak":
                {
                    // remove break lines
                    messages.push(this.filter(trace.payload.message));
                    break;
                }
            case "end":
                {
                    // messages.push("終了");
                    break;
                }
        }
    }

    console.log(`response: `+messages.join(","));
    return messages;
};

module.exports.filter = function filter(string) {
    string = string.replace(/\'/g, '\'')
    string = string.replace(/(<([^>]+)>)/ig, "")
    string = string.replace(/\&/g, ' and ')
    string = string.replace(/[&\\#,+()$~%*?<>{}]/g, '')
    string = string.replace(/\s+/g, ' ').trim()
    string = string.replace(/ +(?= )/g,'')

	return string;
}

module.exports.alexaDetectedEntities = function alexaDetectedEntities(alexaRequest) {
    let entities = [];
    const entitiesDetected = alexaRequest.request.intent.slots;
    for ( const entity of Object.values(entitiesDetected)) {
        entities.push({
            name: entity.name,
            value: entity.value
        });
    }
    return entities;
}

(3)package.jsonの書き換え
 package.jsonを選択・表示させ、以下のコードと置換します。

package.json
{
  "name": "@amzn/hello-world",
  "version": "1.2.0",
  "description": "alexa utility for quickly building skills",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Amazon Alexa",
  "license": "Apache License",
  "dependencies": {
    "ask-sdk-core": "^2.10.0",
    "axios": "^1.6.2"
  },
  "devDependencies": {
    "ask-sdk-local-debug": "^1.1.0"
  }
}

ここまできたら、ウィンドウ右上の「デプロイ」をクリックします。デプロイが完了したら「テスト」タブを選択します。

image.png

5.Alexa環境での実行とVoiceflowとの関係・問題点
5.1 Alexa環境での実行
 作成したスキルは最初に設定したようにスキル名が「接続くん」、呼び出し名が「呼び出しくん」です。そこでAlexa Developer Consoleのテストタブで開いた画面でステージを「開発中」にしたのち、入力ボックスに「呼び出しくん」と入力します。図のように「最初のメッセージです。」との応答が返ってくることでしょう。スキルを終了するには「終了」と入力します。
 
image.png

5.2 AlexaスキルとVoiceflowアシスタントとの関係
 作成したスキルは入出力関係はAlexaが、その後の処理はVoiceflowアシスタントが担当しています。そのため、それによる利点もあれば問題点もあります。
 利点としては入出力関係に変化がなければ、Voiceflowの内容を書き換えてもVoiceflow側で「Publish」操作をしておけばAlexa側では何も変更せずに、変更が反映されることです。この場合にはAPIキーの値が変化しないのでAlexaのコード変更は不要です。例えば、Voiceflowで図のようにSpeakブロックを一つ追加して前のブロックに接続します。この状態でVoiceflow単体で実行して動作確認します。
*Voiceflow側で一度動作させてからでないと、Publish操作してもAlexa側に変更が反映されないようです。

image.png

 この状態で「Publish」操作を行ったのち、Alexa Developer Consoleの「テスト」タブで実行すると、Alexa側では何の修正もなしで、ただちに変更が反映されているのがわかります。
image.png

 また全く別のVoiceflow Assistant(ただしCaptureやChoise,Intentなど音声入出力ブロックは含まないもの)でもAlexa側の変更はutil.jsのAPIキーの部分だけですみます(コード変更後にデプロイ操作は必要)。
 具体的には新規にVoiceflowアシスタントを作成し、Voiceflowで動作確認後にPublish操作でそのAPIキーを取得します。そしてAlexa Developer Consoleのコードエディタでutil.jsのコードを開き、「const VF_API_KEY = " **** ";」の行の" "の内容を取得したAPIキーで置換します。その後にデプロイして「テスト」タブで実行(呼び出し名は「呼び出しくん」のまま)すれば、新たなアシスタントの内容が実行されるのがわかるでしょう。

5.3 AlexaスキルとしてVoiceflowアシスタントを実行するときの注意点
 ここまで説明したようにAlexaは入出力部分を担い、Voiceflowはそれを受けてその下で処理を行っています。そのためVoiceflowのCaptureやChoise,Intentなど入出力を伴うブロックについては、NLUモデル(自然言語処理系)を共通化しておく必要があること、またVoiceflow単体で処理した結果とAlexa環境で処理した結果とでは異なる可能性があることに注意が必要です。この点について説明して行きます。
 
5.3.1 NLUモデルの共通化
 Alexaで受け付けた入力をVoiceflowに届けるためにはインテントやその設定内容を共通化しておく必要があります。これがNLU(Natural Language Understanding)モデルの共通化です。すなわちVoiceflowで設定したインテントすべてについて、Alexa側でも同様に定義します。
*Voiceflowでインテントブロックを使用していない人はここはいったん読み飛ばしていただいても結構です。

(1) Voiceflowのインテント設定例
図に二種類のインテント設定例を示します。なお、インテントの追加はデザイン画面で右クリックして「Add Triger」を選択します。
intent_aは直接、入力フレーズをUtterancesとして{コスモス|ばら|ひまわり}で指定しています。それに対してintent_bは、entity(いわば語彙集合)として "animals"を使用し、Utterancesとしては{{amimals}|{animals}です}を指定しています。また "animals"の内容は{牛|うさぎ|犬|ねこ}です。

image.png

これらを使用して、入力語句がintent_aに属するものなら「intent_aが実行されました。」、intent_bに属するものなら「(入力語句)ですね。」と返すアシスタントは次のようになります。
image.png
(2) Alexa側の設定
 もし上記のアシスタントをAlexa側でNLUモデル共通化をせずに実行すると「最初のメッセージです。」までは実行されても、そのあとの情報がAlexaからVoiceflowに届かず図のようなエラーとなります。
image.png

そこでAlexa側でもNLUモデルをVoiceflow側と共通化します。まずAlexa developer consoleを開き[ビルド]タブ→[インテント]→[インテントを追加]を選択します。

image.png

「カスタムインテントを作成」から Intent_a を作成しサンプル発話(Utterance)として{コスモス|ばら|ひまわり}を登録します。
image.png左上の[保存]をクリックし、再度、左側の「インテント」をクリックして、今度は同様にして intent_b を追加します。ただし今度はentity(語彙集合)があるので少し複雑です。とりあえずサンプル発話の欄にVoiceflowと同様に{animals}と入力します。
image.pngAlexaではVoiceflowのentityに相当する概念はスロットとなります。新規のスロットとなるので「追加」ボタンをクリックします。右側の「+」をクリックして登録、さらにVoiceflow側と同様にサンプル発話に「{animals}(半角スペース) です」も追加して「+」をクリックします。インテントスロットに {animals} とあるのがわかります。

image.png
この段階ではAlexa側の{animals}の内容はカラです。Voiceflowでは直接{animals}に単語を収納しましたが、Alexaでは別にスロットタイプと呼ばれる語彙群を作成して、それをスロットに割り当てます。
 左側の「スロットタイプ」を選びカスタムスロットタイプとして「animal_words」を作成します。
 
image.png
「次へ」をクリックして、スロット値(語彙)として単語を登録してゆきます。単語登録が終わったら、スロット {animals} とスロットタイプ {animal_words} の紐付け作業です。

image.png左側の「インテント」→「intent_b」を選択し、Intent_b編集画面の下部にある「インテントスロット」でスロット {animals} にスロットタイプ {animal_words}を設定します。

image.png

これで見かけ上の対応は終わりなのですが、先ほど書き換えたAlexaのコードはインテントスロットがあることを前提としているので、インテントスロットが存在しないとうまく動作しません。そこでintent_aを編集してインテントスロットにダミーのスロットを追加します。図ではインテントスロットとして「flowername」、スロットタイプとして「AMAZON.FOOD」を指定していますが、utteranceの部分で{flowername}は用いていないのでダミーであることがわかります。

image.png

そうしたら変更を反映させるために右上の[スキルのビルド]ボタンをクリックしてしばらく待ちます。ダミーのスロットについて「使用されていない」との警告が出ますが問題はありません。

*サンプル発話で「{animals} です」のようにスロット+日本語の場合、スロット+(半角スペース)+「句点なしの日本語」としてください。「{animals}です」(半角空白なし)、「{animals} です。」(句点あり)とすると、「スキルのビルド」で次のような文を含むエラーが出ます。
・Sample utterances can consist of only unicode characters, spaces, periods for abbreviations, underscores, possessive apostrophes, and hyphens.
・Parsing error in sample. 

ここで、すぐに「テスト」で動作確認をしたいところですが、その前に入力語句が正しくインテントと結びつけられているかの確認を行います。ビルドのウィンドウの上部にある「モデルの評価(またはEvaluate model)」をクリックします。中央の部分に、語句を入力し、それがどのインテントと結びつけられスロット値として何が得られたかがわかります。
 図の例では「ねこ」をsubmitするとintent_bに結びつけられ、animalsのスロット値として「ねこ」がえられることがわかります。思うように認識されない場合はNLUモデルの修整を行います。

image.png

当然のことですが、この判断はAlexa側で行っているので、VoiceflowのCaptureの結果とは微妙に異なります。Voiceflowは一致度を厳しく判定し、Alexaは語尾などの表現が一致していれば登録されていない語句でも受け入れるようです。

(3) Alexa Developer Consoleでの実行
 NLUモデルを合わせて、Alexa側で実行させると次のように無事実行されます。
image.png

5.3.2 注意点のまとめ
 Alexa側のコードを細かくいじれば、Voiceflowとの対応を高めることはできるかもしれませんが、それではAlxaスキルを簡単に開発したいという目的と矛盾してしまいます。そのため上記の方法で、Voiceflowを利用してAlexaスキルを開発するには以下の点に留意が必要です。

(1)Alexaでの入出力はAlexaが管理しているので、同じ入力でもAlexaとVoiceflowとでは処理結果が異なることがあります。

(2)VoiceflowのCaptureやChoiceブロックは、いったんAlexaに音声制御が移るので、その後に連続するブロックは実行されず、音声入力が終わると合致するintentにジャンプします。
これは大きな違いで、あらかじめこの点を考慮してVoiceflowのアシスタントを構成しないと、Alexaでの実行が困難となります。たとえばVoiceflowであれば、入力語句を反復発音させるにはCaptureブロックにSpeakブロックをつなげるだけですみますが、Alexaで実行するには面倒でもさきほどのintent_bの例のようにいったんintentにジャンプさせなければなりません。

(3)VoiceflowのImageブロックはAlexaでは無視されます。

6.最後に
 Voiceflowは頻繁に更新が行われ、昨秋の更新に引き続き、この5月にも更新がおこなわれインターフェースが変更となりました。当初のAlexaやGoogleデバイスのソフト開発ツールの位置づけから、汎用的なAI関連独立ソフトへの指向を強めているように感じます。

 最後に本文中の至らない点は下記までご連絡いただければ幸いです。
image.png
できる限り内容に反映させます。ただし大幅な更新があると対応に時間がかかる点はご了承ください。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?