実装のヒント(Dialogflow)

以下のヒントを参考にして、会話デザインのおすすめの方法をアクションに実装してください。

バリエーションを想定する

これは Dialogflow で、「ユーザーの発言内容」入力で処理します。また、同じアクションにマッピングできる複数のインテントを使用して、各インテントを異なる「ユーザーの発話」フレーズのセットでトリガーできるようにします。

有用な再プロンプトを提供して失敗時に適切な処理を行う

アクションは、入力を受け取らなかったり(未入力)か、ユーザーの入力を理解できなかった(不一致)ために、処理を続行できないことがあります。この場合、アシスタントはまず、ユーザーが別のアクションをトリガーする必要があるかどうかを判断します。アシスタントがユーザーの入力と別のアクションと一致しない場合、ユーザーはアクションのコンテキストで操作を続行します。このシナリオはいつでも発生する可能性があるため、フォールバックを使用して会話の各ターンで入力なし / 不一致状況を一意に処理することをおすすめします。フォールバックを使用すると、ユーザーが元の状態に戻るのをサポートできます。

これを行うには、conv.data オブジェクトの fallbackCount 変数を初期化し、0 に設定します。2 つのフォールバック プロンプト(明確にするためのエスカレーション)と、会話を終了する最後のフォールバック プロンプトの配列を準備します。

次に、フォールバック インテントを作成します(理想的には、エージェントの実行可能なインテントごとに 1 つ)。インテント ハンドラで、conv.data オブジェクトからフォールバック カウントを取得して増分し、3 未満の場合は、3 の配列からプロンプトを取得します。カウントが 4 以上の場合は、最後のプロンプトを使用して会話を閉じます。フォールバック以外のすべてのインテントで、フォールバック カウントを 0 にリセットします。理想的には、特定のインテントのフォールバックをインテント固有にするためテンプレート化します。

Node.js

const GENERAL_FALLBACK = [
   'Sorry, what was that?',
   'I didn\'t quite get that. I can help you find good local restaurants, what do you want to know about?',
];

const LIST_FALLBACK = [
   'Sorry, what was that?',
   'I didn\'t catch that. Could you tell me which one you prefer?',
];

const FINAL_FALLBACK = 'I\'m sorry I\'m having trouble here. Let\'s talk again later.';

const handleFallback = (conv, promptFetch, callback) => {
 conv.data.fallbackCount = parseInt(conv.data.fallbackCount, 10);
 conv.data.fallbackCount++;
 if (conv.data.fallbackCount > 2) {
   conv.close(promptFetch.getFinalFallbackPrompt());
 } else {
   callback();
 }
}
// Intent handlers below
const generalFallback = (conv) => {
  handleFallback = (conv, promptFetch, () => {
    conv.ask(GENERAL_FALLBACK[conv.data.fallbackCount],
      getGeneralNoInputPrompts());
 });
}

const listFallback = (conv) => {
  handleFallback = (conv, promptFetch, () => {
   conv.ask(LIST_FALLBACK[conv.data.fallbackCount],
       getGeneralNoInputPrompts());
 });
}

const nonFallback = (conv) => {
  conv.data.fallbackCount = 0;
  conv.ask('A non-fallback message here');
}

Java

private static final List<String> GENERAL_FALLBACK =
    Arrays.asList(
        "Sorry, what was that?",
        "I didn\'t quite get that. I can tell you all about IO, like date or location, or about the sessions. What do you want to know about?");
private static final List<String> LIST_FALLBACK =
    Arrays.asList(
        "Sorry, what was that?",
        "I didn\'t catch that. Could you tell me which one you liked?");
private static final List<String> FINAL_FALLBACK =
    Arrays.asList("I\'m sorry I\'m having trouble here. Maybe we should try this again later.");

@ForIntent("General Fallback")
public ActionResponse generalFallback(ActionRequest request) {
  ResponseBuilder responseBuilder = getResponseBuilder(request);
  int fallbackCount = (Integer) request.getConversationData().get("fallbackCount");
  fallbackCount++;
  request.getConversationData().put("fallbackCount", fallbackCount);
  if (fallbackCount > 2) {
    responseBuilder.add(getRandomPromptFromList(FINAL_FALLBACK)).endConversation();
  } else {
    responseBuilder.add(getRandomPromptFromList(GENERAL_FALLBACK));
  }
  return responseBuilder.build();
}

private String getRandomPromptFromList(List<String> prompts) {
  Random rand = new Random();
  int i = rand.nextInt(prompts.size());
  return prompts.get(i);
}

@ForIntent("List Fallback")
public ActionResponse listFallback(ActionRequest request) {
  ResponseBuilder responseBuilder = getResponseBuilder(request);
  int fallbackCount = (Integer) request.getConversationData().get("fallbackCount");
  fallbackCount++;
  request.getConversationData().put("fallbackCount", fallbackCount);
  if (fallbackCount > 2) {
    responseBuilder.add(getRandomPromptFromList(FINAL_FALLBACK)).endConversation();
  } else {
    responseBuilder.add(getRandomPromptFromList(LIST_FALLBACK));
  }
  return responseBuilder.build();
}

@ForIntent("Non Fallback")
public ActionResponse nonFallback(ActionRequest request) {
  ResponseBuilder responseBuilder = getResponseBuilder(request);
  request.getConversationData().put("fallbackCount", 0);
  responseBuilder.add("Non Fallback message");
  return responseBuilder.build();
}

いつでも手伝うことができるようにする

「何ができる?」などのヘルプフレーズをリッスンするインテントを作成します。「どんなことを言えばいいですか」、「サポートして」などのフレーズを言います。このインテントでは、エージェントができることの概要を示し、ユーザーに可能なアクションを案内する(ローテーションされた)レスポンスを提供します。Dialogflow でフォローアップ ヘルプ インテントを使用して、実行可能なインテントごとに異なるヘルプシナリオを作成するのが理想的です。

Node.js

const HELP_PROMPTS = [
   'There\'s a lot you might want to know about the local restaurants, and I can tell you all about it, like where it is and what kind of food they have. What do you want to know?',
   'I\'m here to help, so let me know if you need any help figuring out where or what to eat. What do you want to know?',
];

// Intent handler
const help = (conv) => {
 reply(conv, promptFetch.getHelpPrompt(), // fetches random entry from HELP_PROMPTS
     promptFetch.getGeneralNoInputPrompts());
}

Java

private static final List<String> HELP_PROMPTS =
    Arrays.asList(
        "There's a lot you might want to know about IO, and I can tell you all about it, like where it is and what the sessions are. What do you want to know?",
        "IO can be a little overwhelming, so I\'m here to help. Let me know if you need any help figuring out the event, like when it is, or what the sessions are. What do you want to know?");

@ForIntent("Help")
public ActionResponse help(ActionRequest request) {
  return getResponseBuilder(request).add(getRandomPromptFromList(HELP_PROMPTS)).build();
}

ユーザーが情報を再度聞けるようにする

すべての app.ask(output) メソッドを、conv.data.lastPrompt に出力を追加するプロキシ関数でラップします。「何?」といったユーザーからの繰り返しのプロンプトをリッスンする 繰り返しインテントを作成します「もう一度言って」 「繰り返してもらえますか?」ユーザーが繰り返すことを要求したことを認識するために使用できる、繰り返し接頭辞の配列を作成します。繰り返しインテント ハンドラで、繰り返し接頭辞と conv.data.lastPrompt の値を連結した文字列で ask() を呼び出します。最後のプロンプトで SSML 開始タグを使用した場合は、すべての SSML 開始タグをシフトする必要があることにご注意ください。

Node.js

const REPEAT_PREFIX = [
    'Sorry, I said ',
    'Let me repeat that. ',
];

const reply = (conv, inputPrompt, noInputPrompts) => {
  conv.data.lastPrompt = inputPrompt;
  conv.data.lastNoInputPrompts = noInputPrompts;
  conv.ask(inputPrompt, noInputPrompts);
}
// Intent handlers
const normalIntent = (conv) => {
  reply(conv, 'Hey this is a question', SOME_NO_INPUT_PROMPTS);
}

const repeat = (conv) => {
  let repeatPrefix = promptFetch.getRepeatPrefix(); // randomly chooses from REPEAT_PREFIX
  // Move SSML start tags over
  if (conv.data.lastPrompt.startsWith(promptFetch.getSSMLPrefix())) {
    conv.data.lastPrompt =
        conv.data.lastPrompt.slice(promptFetch.getSSMLPrefix().length);
    repeatPrefix = promptFetch.getSSMLPrefix() + repeatPrefix;
  }
  conv.ask(repeatPrefix + conv.data.lastPrompt,
      conv.data.lastNoInputPrompts);
}

Java

private final List<String> REPEAT_PREFIX = Arrays.asList("Sorry, I said ", "Let me repeat that.");

private final String SsmlPrefix = "<speak>";

@ForIntent("Normal Intent")
public ActionResponse normalIntent(ActionRequest request) {
  ResponseBuilder responseBuilder = getResponseBuilder(request);
  responseBuilder.getConversationData().put("lastPrompt", "Hey this is a question");
  return responseBuilder.build();
}

@ForIntent("repeat")
public ActionResponse repeat(ActionRequest request) {
  ResponseBuilder responseBuilder = getResponseBuilder(request);
  String repeatPrefix = getRandomPromptFromList(REPEAT_PREFIX);
  // Move SSML start tags over
  String lastPrompt = (String) responseBuilder.getConversationData().get("lastPrompt");
  if (lastPrompt.startsWith(SsmlPrefix)) {
    String newLastPrompt = lastPrompt.substring(SsmlPrefix.length());
    responseBuilder.getConversationData().put("lastPrompt", newLastPrompt);
    repeatPrefix = SsmlPrefix + repeatPrefix;
  }
  responseBuilder.add(repeatPrefix + lastPrompt);
  return responseBuilder.build();
}

ユーザーの好みに合わせて会話をカスタマイズする

アクションでユーザーに対し好みを尋ね、後で使用できるようにその内容を記憶できます。これにより、ユーザーに合わせてそれ以降の会話をカスタマイズできます。

この例のアクションは、郵便番号に対応する天気予報をユーザーに提供します。次のサンプルコードは、後の会話で使用できるようにアクションでユーザーの郵便番号を記憶しておくかどうかをユーザーに尋ねます。

Node.js

app.intent('weather_report', (conv) => {
  let zip = conv.arguments.get('zipcode');
  conv.data.zip = zip;
  conv.ask(getWeatherReport(zip));
  conv.ask(new Confirmation(`Should I remember ${zip} for next time?`));
});

app.intent('remember_zip', (conv, params, confirmation) => {
  if (confirmation) {
    conv.user.storage.zip = conv.data.zip;
    conv.close('Great! See you next time.');
  } else conv.close('Ok, no problem.');
});

Java

@ForIntent("weather_report")
public ActionResponse weatherReport(ActionRequest request) {
  ResponseBuilder responseBuilder = getResponseBuilder(request);
  String zip = (String) request.getArgument("location").getStructuredValue().get("zipCode");
  responseBuilder.getConversationData().put("zip", zip);
  responseBuilder.add(getWeatherReport(zip));
  responseBuilder.add(
      new Confirmation().setConfirmationText("Should I remember " + zip + " for next time?"));
  return responseBuilder.build();
}

@ForIntent("remember_zip")
public ActionResponse rememberZip(ActionRequest request) {
  ResponseBuilder responseBuilder = getResponseBuilder(request);
  if (request.getUserConfirmation()) {
    responseBuilder.getUserStorage().put("zip", responseBuilder.getConversationData().get("zip"));
    responseBuilder.add("Great! See you next time.").endConversation();
  } else {
    responseBuilder.add("Ok, no problem.").endConversation();
  }
  return responseBuilder.build();
}

最初のダイアログでユーザーに郵便番号を尋ねた後、次の呼び出しではプロンプトをスキップして同じ郵便番号を使用できます。引き続きエスケープ ルート(別の郵便番号を選択できる候補チップなど)を提供する必要がありますが、一般的なケースでは会話の順番を減らすことで、よりシームレスなエクスペリエンスを実現できます。

Node.js

app.intent('weather_report', (conv) => {
  let zip = conv.arguments.get('zipcode');
  if (zip) {
    conv.close(getWeatherReport(zip));
  } else if (conv.user.storage.zip) {
    conv.ask(new SimpleResponse(getWeatherReport(conv.user.storage.zip)));
    conv.ask(new Suggestions('Try another zipcode'));
  } else {
    conv.ask('What\'s your zip code?');
  }
});

app.intent('provide_zip_df', (conv) => {
  conv.user.storage.zip = conv.arguments.get('zipcode');
  conv.close(getWeatherReport(conv.user.storage.zip));
});

Java

public ActionResponse weatherReport2(ActionRequest request) {
  ResponseBuilder responseBuilder = getResponseBuilder(request);
  String zip = (String) request.getArgument("location").getStructuredValue().get("zipCode");
  if (zip != null) {
    responseBuilder.add(getWeatherReport(zip)).endConversation();
  } else if ((zip = (String) responseBuilder.getUserStorage().get("zip")) != null) {
    responseBuilder.add(new SimpleResponse().setTextToSpeech(getWeatherReport(zip)));
    responseBuilder.add(new Suggestion().setTitle("Try another zipcode"));
  } else {
    responseBuilder.add("What's your zip code?");
  }
  return responseBuilder.build();
}

リピーター向けにカスタマイズする

会話と会話の間になんらかの状態を保持することで、リピーターにとって非常に自然なエクスペリエンスが保証されます。このエクスペリエンスを実現するための最初のステップは、リピーターごとに異なる挨拶をすることです。たとえば、挨拶文を短くしたり、過去の会話に基づいて有用な情報を表示したりできます。これを行うには、受信した AppRequest.User lastSeen プロパティを使用して、ユーザーが以前にアクションとやり取りしたことがあるかどうかを判断します。lastSeen プロパティがリクエスト ペイロードに含まれている場合は、通常とは異なる挨拶を使用できます。

以下のコードは、Node.js クライアント ライブラリを使用して last.seen の値を取得します。

Node.js

// This function is used to handle the welcome intent
// In Dialogflow, the Default Welcome Intent ('input.welcome' action)
// In Actions SDK, the 'actions.intent.MAIN' intent
const welcome = (conv) => {
  if (conv.user.last.seen) {
    conv.ask(`Hey you're back...`);
  } else {
    conv.ask('Welcome to World Cities Trivia!...');
  }
}

Java

// This function is used to handle the welcome intent
// In Dialogflow, the Default Welcome Intent ('input.welcome' action)
// In Actions SDK, the 'actions.intent.MAIN' intent
public ActionResponse welcome(ActionRequest request) {
  ResponseBuilder responseBuilder = getResponseBuilder(request);
  if (request.getUser().getLastSeen() != null) {
    responseBuilder.add("Hey you're back...");
  } else {
    responseBuilder.add("Welcome to Number Genie!...");
  }
  return responseBuilder.build();
}

lastSeen の実際の値に合わせてレスポンスを調整することで、この挨拶をさらに拡張できます。たとえば、現在のインタラクションから何か月も前に最後のインタラクションが発生したユーザーには、その前日にアクションを使用したユーザーとは異なる挨拶メッセージを受け取る可能性があります。

会話中の音量調節

サポートされているデバイスでは、ユーザーは「音量を上げて」や「音量を 50% にして」などと話しかけて、会話アクション内でデバイスの音量を調整できます。類似のトレーニング フレーズを処理するインテントがある場合は、インテントが優先されます。アクションに特別な理由がない限り、アシスタントにこれらのユーザー リクエストを処理させることをおすすめします。