VoxaAI/voxa-cli

View on GitHub
src/AlexaSchema.ts

Summary

Maintainability
A
2 hrs
Test Coverage
/*
 * Copyright (c) 2018 Rain Agency <contact@rain.agency>
 * Author: Rain Agency <contact@rain.agency>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
/* tslint:disable:no-empty no-submodule-imports */
import _ from "lodash";
import path from "path";
import uuid from "uuid/v5";
import {
  IDialog,
  IDialogIntent,
  IDialogSlot,
  IDialogSlotPrompt,
  IInteractionModel,
  IPrompt,
  IVariation
} from "./AlexaInterfaces";
import { IIntent, ISlotDefinition, Schema } from "./Schema";
import { IVoxaSheet } from "./VoxaSheet";

const NAMESPACE = "alexa";
const AVAILABLE_LOCALES = [
  "en-US",
  "en-GB",
  "en-CA",
  "en-AU",
  "en-IN",
  "de-DE",
  "ja-JP",
  "es-ES",
  "es-MX",
  "es-US",
  "fr-FR",
  "fr-CA",
  "it-IT",
  "pt-BR"
];

export class AlexaSchema extends Schema {
  public environment = "staging";

  constructor(voxaSheets: IVoxaSheet[], interactionOptions: any) {
    super(NAMESPACE, AVAILABLE_LOCALES, voxaSheets, interactionOptions);
  }

  public validate() {}

  public build(locale: string, environment: string) {
    this.buildLanguageModel(locale, environment);
    this.buildPublishing(environment);
  }

  public buildPublishing(environment: string) {
    const manifest = this.mergeManifest(environment);
    _.set(manifest, "manifestVersion", "1.0");

    this.fileContent.push({
      path: path.join(
        this.interactionOptions.rootPath,
        this.interactionOptions.speechPath,
        this.NAMESPACE,
        `${_.kebabCase(environment)}-manifest.json`
      ),
      content: { manifest }
    });
  }

  public contentLanguageModel(locale: string, environment: string): IInteractionModel {
    const invocation = _.find(this.invocations, { locale, environment });
    const invocationName = _.get(invocation, "name", "Skill with no name");

    const intents = this.getIntentsDefinition(locale, environment);
    const types = this.getSlotsByIntentsDefinition(locale, environment).map(rawSlot => ({
      name: rawSlot.name,
      values: rawSlot.values.map(value => ({ name: value }))
    }));

    return {
      interactionModel: {
        languageModel: { invocationName, intents, types },
        dialog: this.getDialogForLocaleAndEnvironment(locale, environment),
        prompts: this.getPromptsForLocaleAndEnvironment(locale, environment)
      }
    };
  }

  public buildLanguageModel(locale: string, environment: string) {
    this.fileContent.push({
      path: path.join(
        this.interactionOptions.rootPath,
        this.interactionOptions.speechPath,
        this.NAMESPACE,
        locale,
        `${_.kebabCase(environment)}-interaction.json`
      ),
      content: this.contentLanguageModel(locale, environment)
    });

    const canFulfillIntents = _.chain(this.intentsByPlatformAndEnvironments(locale, environment))
      .filter("canFulfillIntent")
      .map("name")
      .value();

    this.fileContent.push({
      path: path.join(
        this.interactionOptions.rootPath,
        this.interactionOptions.contentPath,
        `${_.kebabCase(environment)}-canfulfill-intents.json`
      ),
      content: canFulfillIntents
    });
  }

  private getDialogForLocaleAndEnvironment(
    locale: string,
    environment: string
  ): IDialog | undefined {
    const dialog: IDialog | undefined = {
      intents: this.generateDialogModel(this.intentsByPlatformAndEnvironments(locale, environment)),
      delegationStrategy: "SKILL_RESPONSE"
    };

    if (!dialog.intents.length) {
      return;
    }

    return dialog;
  }

  private getPromptsForLocaleAndEnvironment(
    locale: string,
    environment: string
  ): IPrompt[] | undefined {
    const prompts: IPrompt[] | undefined = this.generatePrompts(
      this.intentsByPlatformAndEnvironments(locale, environment)
    );
    if (!prompts.length) {
      return;
    }

    return prompts;
  }

  private generateDialogModel(intents: IIntent[]): IDialogIntent[] {
    return _(intents)
      .filter(intentOrSlotRequireDialog)
      .map(
        (intent): IDialogIntent => {
          const prompts: IDialogSlotPrompt = {};
          if (intent.confirmations.length > 0) {
            prompts.confirmation = getPromptId("Confirmation", "Intent", intent.confirmations);
          }
          return {
            name: intent.name,
            delegationStrategy: intent.delegationStrategy,
            confirmationRequired: intent.confirmationRequired,
            slots: this.generateDialogSlotModel(intent.slotsDefinition),
            prompts
          };
        }
      )
      .value();
  }

  private generateDialogSlotModel(slots: ISlotDefinition[]): IDialogSlot[] {
    return _(slots)
      .map(
        (slot: ISlotDefinition): IDialogSlot => {
          const prompts: IDialogSlotPrompt = {};
          if (slot.prompts.elicitation.length > 0) {
            prompts.elicitation = getPromptId("Elicitation", "Slot", slot.prompts.elicitation);
          }

          if (slot.prompts.confirmation.length > 0) {
            prompts.confirmation = getPromptId("Confirmation", "Slot", slot.prompts.confirmation);
          }

          return {
            name: slot.name.replace("{", "").replace("}", ""),
            type: slot.type,
            elicitationRequired: slot.requiresElicitation,
            confirmationRequired: slot.requiresConfirmation,
            prompts
          };
        }
      )
      .value();
  }

  private generatePrompts(intents: IIntent[]): IPrompt[] {
    const intentPrompts: IPrompt[] = _(intents)
      .filter(intentHasPrompts)
      .map(
        (intent: IIntent): IPrompt =>
          getPromptsObject("Confirmation", "Intent", intent.confirmations)
      )
      .value();

    const slotPrompts = this.generateSlotPrompts(intents);

    return _.concat(intentPrompts, slotPrompts);
  }

  private generateSlotPrompts(intents: IIntent[]): IPrompt[] {
    return _(intents)
      .map("slotsDefinition")
      .flatten()
      .filter(slotHasPrompts)
      .map((slot: ISlotDefinition): IPrompt[] => {
        const prompts: IPrompt[] = [];
        if (slot.prompts.confirmation.length > 0) {
          prompts.push(getPromptsObject("Confirmation", "Slot", slot.prompts.confirmation));
        }

        if (slot.prompts.elicitation.length > 0) {
          prompts.push(getPromptsObject("Elicitation", "Slot", slot.prompts.elicitation));
        }

        return prompts;
      })
      .flatten()
      .value();
  }
}

function hashObj(obj: any): string {
  return uuid(JSON.stringify(obj), uuid.DNS);
}

function intentOrSlotRequireDialog(intent: IIntent): boolean {
  if (intent.confirmationRequired || intent.delegationStrategy) {
    return true;
  }

  const slotRequiresDialog = _(intent.slotsDefinition)
    .map(slot => slot.requiresElicitation || slot.requiresConfirmation)
    .some();

  return slotRequiresDialog;
}

function slotHasPrompts(slot: ISlotDefinition): boolean {
  return slot.prompts.confirmation.length > 0 || slot.prompts.elicitation.length > 0;
}

function intentHasPrompts(intent: IIntent): boolean {
  return intent.confirmations.length > 0;
}

function getPromptsObject(
  dialogType: "Elicitation" | "Confirmation",
  objectType: "Slot" | "Intent",
  data: string[]
): IPrompt {
  return {
    id: getPromptId(dialogType, objectType, data),
    variations: formatVariations(data)
  };
}

function getPromptId(
  dialogType: "Elicitation" | "Confirmation",
  objectType: "Slot" | "Intent",
  data: string[]
): string {
  return `${dialogType}.${objectType}.${hashObj(data)}`;
}

function formatVariations(variations: string[]): IVariation[] {
  return _.map(
    variations,
    (variation): IVariation => ({
      type: "PlainText",
      value: variation
    })
  );
}