VoxaAI/voxa-cli

View on GitHub
src/Processor.ts

Summary

Maintainability
D
2 days
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-submodule-imports */
import _ from "lodash";
import {
  IDownload,
  IIntent,
  IInvocation,
  IPublishingInformation,
  ISlot,
  ISlotDefinition,
  ISlotSynonymns,
  IView
} from "./Schema";
import { getSheetType, IVoxaSheet, SheetTypes } from "./VoxaSheet";

export function sheetLocale(voxaSheet: IVoxaSheet, AVAILABLE_LOCALES: string[]) {
  let locale = AVAILABLE_LOCALES.find((loc: string) => {
    let lastDotIndex = _.lastIndexOf(voxaSheet.spreadsheetTitle, ".");
    lastDotIndex = lastDotIndex === -1 ? voxaSheet.spreadsheetTitle.length : lastDotIndex;

    const titleWithoutExtension = _.toLower(voxaSheet.spreadsheetTitle.slice(0, lastDotIndex));
    return _.endsWith(titleWithoutExtension, _.toLower(loc));
  });
  locale = locale || AVAILABLE_LOCALES[0];

  return locale;
}

export function downloadProcessor(voxaSheets: IVoxaSheet[], AVAILABLE_LOCALES: string[]) {
  const voxaSheetsDownloads = voxaSheets.filter(voxaSheet =>
    _.includes([SheetTypes.DOWNLOAD], getSheetType(voxaSheet))
  );

  return voxaSheetsDownloads.map((voxaSheet: IVoxaSheet) => {
    const locale = sheetLocale(voxaSheet, AVAILABLE_LOCALES);
    const sheetPlaceholder = getSheetType(voxaSheet);
    const name = (voxaSheet.sheetTitle = voxaSheet.sheetTitle.replace(sheetPlaceholder, ""));
    const data = _.chain(voxaSheet.data)
      .filter(item => {
        const tempItem = _.omitBy(item, _.isEmpty);
        return !_.isEmpty(tempItem);
      })
      .value();
    const download: IDownload = { name, data, locale };
    return download;
  });
}

export function invocationProcessor(voxaSheets: IVoxaSheet[], AVAILABLE_LOCALES: string[]) {
  const voxaSheetsInvocations = voxaSheets.filter(voxaSheet =>
    _.includes([SheetTypes.INVOCATION], getSheetType(voxaSheet))
  );

  return voxaSheetsInvocations.reduce(
    (acc, voxaSheet: IVoxaSheet) => {
      const locale = sheetLocale(voxaSheet, AVAILABLE_LOCALES);
      voxaSheet.data.map((item: any) => {
        const { environment, invocationName } = item;
        acc.push({ name: invocationName, environment, locale });
      });

      return acc;
    },
    [] as IInvocation[]
  );
}

export function viewsProcessor(voxaSheets: IVoxaSheet[], AVAILABLE_LOCALES: string[]) {
  function sanitizeView(text: string = "") {
    return text
      .replace(/’/g, "'")
      .replace(/’/g, "'")
      .replace(/“/g, '"')
      .replace(/”/g, '"')
      .replace(/&/g, "and");
  }

  const voxaSheetsViews = voxaSheets.filter(voxaSheet =>
    _.includes([SheetTypes.VIEWS], getSheetType(voxaSheet))
  );

  return voxaSheetsViews.map((voxaSheet: IVoxaSheet) => {
    const locale = voxaSheet.sheetTitle.split("@")[1] || AVAILABLE_LOCALES[0];
    const data = _.chain(voxaSheet.data)
      .reduce((acc, view) => {
        const { path } = view;
        const pathLowerCase = _.toLower(path) as string;
        let { value } = view;
        if (_.isEmpty(path)) {
          return acc;
        }
        const shouldBeArray = [".text", ".say", ".reprompt", ".tell", ".ask"].find(suffix =>
          path.includes(suffix)
        );
        const isGA = [".ga"].find(option => _.endsWith(pathLowerCase, option));
        const isASuggestionChip = [".dialogflowsuggestions", ".facebooksuggestionchips"].find(
          option => _.endsWith(pathLowerCase, option)
        );

        if (shouldBeArray && _.isString(value) && !_.isEmpty(value)) {
          const temp = _.get(acc, path, []) as string[];
          temp.push(sanitizeView(value));
          value = temp;
        }

        if (!_.isEmpty(value) && isASuggestionChip) {
          value = value.split("\n").map((v: string) => v.trim());
        }

        if (!_.isEmpty(value) && isGA) {
          value = value.split("\n").reduce(
            (accGA: { ec: string; ea: string; el: string }, next: string, index: number) => {
              if (index === 0) {
                accGA.ec = next;
                accGA.ea = next;
              }

              if (index === 1) {
                accGA.ea = next;
              }

              if (index === 2) {
                accGA.el = next;
              }

              return accGA;
            },
            {} as any
          );

          const temp = _.get(acc, path, []) as string[];
          temp.push(value);
          value = temp;
        }

        _.set(acc, path, value);
        return acc;
      }, {})
      .value();
    const viewResult: IView = { data, locale };
    return viewResult;
  });
}

export function slotProcessor(voxaSheets: IVoxaSheet[], AVAILABLE_LOCALES: string[]) {
  const voxaSheetsSlots = voxaSheets.filter(voxaSheet =>
    _.includes([SheetTypes.SLOTS], getSheetType(voxaSheet))
  );

  return voxaSheetsSlots.map((voxaSheet: IVoxaSheet) => {
    const locale = sheetLocale(voxaSheet, AVAILABLE_LOCALES);
    const name = voxaSheet.sheetTitle;
    const values = _.chain(voxaSheet.data)
      .groupBy("synonym")
      .toPairs()
      .reduce(
        (acc, slot) => {
          const key = slot[0];
          const synonyms = slot[1] || [];

          if (key === undefined || key === "undefined") {
            acc.push(synonyms.map(synonymName => ({ value: synonymName[name], synonyms: [] })));
          } else {
            acc.push({ value: key, synonyms: _.map(synonyms, name) });
          }
          return acc;
        },
        [] as {}[]
      )
      .flattenDeep()
      .filter("value")
      .uniq()
      .value() as ISlotSynonymns[];
    const slotResult: ISlot = { name, values, locale };
    return slotResult;
  });
}

export function intentUtterProcessor(voxaSheets: IVoxaSheet[], AVAILABLE_LOCALES: string[]) {
  const voxaSheetsIntent = filterSheets(voxaSheets, [SheetTypes.INTENT]);

  const voxaSheetsUtter = _.reduce(
    filterSheets(voxaSheets, [SheetTypes.UTTERANCE]),
    reduceIntent("utterance"),
    [] as IVoxaSheet[]
  );

  const voxaSheetResponses = _.reduce(
    filterSheets(voxaSheets, [SheetTypes.RESPONSES]),
    reduceIntent("response"),
    [] as IVoxaSheet[]
  );

  const voxaSheetPrompts = _.reduce(
    filterSheets(voxaSheets, [SheetTypes.PROMPTS]),
    reduceIntent("prompt"),
    [] as IVoxaSheet[]
  );

  const result = _.chain(voxaSheetsIntent)
    .map((voxaSheetIntent: IVoxaSheet) => {
      const locale = sheetLocale(voxaSheetIntent, AVAILABLE_LOCALES);
      let previousIntent: string;
      voxaSheetIntent.data = _.chain(voxaSheetIntent.data)
        .map(row => {
          const info = _.pick(row, [
            "Intent",
            "slotType",
            "slotName",
            "slotRequired",
            "environment",
            "platformIntent",
            "events",
            "canFulfillIntent",
            "startIntent",
            "signInRequired",
            "endIntent",
            "platformSlot",
            "webhookForSlotFilling",
            "webhookUsed",
            "delegationStrategy",
            "confirmationRequired",
            "slotConfirmationRequired",
            "slotElicitationRequired",
            "parameterName",
            "parameterValue"
          ]);
          previousIntent = _.isEmpty(info.Intent) ? previousIntent : info.Intent;
          info.Intent = previousIntent;

          return info;
        })
        .uniq()
        .groupBy("Intent")
        .toPairs()
        .reduce(
          (acc: IIntent[], item: any) => {
            const intentName = item[0] as string;
            const head = _.head(item[1]);
            const events: string[] = splitValues(head, "events");
            const environments: string[] = splitValues(head, "environment");
            const platforms: string[] = splitValues(head, "platformIntent", true);
            const signInRequired = _.get(head, "signInRequired", false) as boolean;

            const webhookForSlotFilling = (_.get(head, "webhookForSlotFilling", false) ||
              _.get(head, "useWebhookForSlotFilling", false)) as boolean;
            const webhookUsed = !!_.get(head, "webhookUsed", true) as boolean;
            const canFulfillIntent = _.get(head, "canFulfillIntent", false) as boolean;
            const startIntent = _.get(head, "startIntent", false) as boolean;
            const endIntent = _.get(head, "endIntent", false) as boolean;
            const confirmationRequired = !!_.get(head, "confirmationRequired", false) as boolean;
            const delegationStrategy = _.get(head, "delegationStrategy");

            const parameterName = _.get(head, "parameterName");
            const parameterValue = _.get(head, "parameterValue");

            const samples = getIntentValueList(
              voxaSheetsUtter,
              voxaSheetIntent.spreadsheetId,
              intentName,
              "utterance"
            );

            const responses = getIntentValueList(
              voxaSheetResponses,
              voxaSheetIntent.spreadsheetId,
              intentName,
              "response"
            );

            const confirmations = getIntentValueList(
              voxaSheetPrompts,
              voxaSheetIntent.spreadsheetId,
              `${intentName}/confirmation`,
              "prompt"
            );

            const slotsDefinition: ISlotDefinition[] = _.chain(item[1])
              .filter("slotName")
              .map(
                (slot: any): ISlotDefinition => ({
                  name: slot.slotName,
                  type: slot.slotType,
                  platform: slot.platformSlot,
                  required: !!slot.slotRequired || false,
                  requiresConfirmation: !!slot.slotConfirmationRequired || false,
                  requiresElicitation: !!slot.slotElicitationRequired || false,
                  samples: getIntentValueList(
                    voxaSheetsUtter,
                    voxaSheetIntent.spreadsheetId,
                    `${intentName}/${slot.slotName}`,
                    "utterance"
                  ),
                  prompts: {
                    confirmation: getIntentValueList(
                      voxaSheetPrompts,
                      voxaSheetIntent.spreadsheetId,
                      `${intentName}/${slot.slotName}/confirmation`,
                      "prompt"
                    ),
                    elicitation: getIntentValueList(
                      voxaSheetPrompts,
                      voxaSheetIntent.spreadsheetId,
                      `${intentName}/${slot.slotName}/elicitation`,
                      "prompt"
                    )
                  }
                })
              )
              .compact()
              .uniq()
              .value();

            const intent: IIntent = {
              name: intentName,
              samples,
              responses,
              slotsDefinition,
              webhookForSlotFilling,
              webhookUsed,
              canFulfillIntent,
              startIntent,
              endIntent,
              events,
              confirmations,
              environments,
              platforms,
              locale,
              signInRequired,
              confirmationRequired,
              delegationStrategy,
              parameterName,
              parameterValue
            };

            acc.push(intent);
            return acc;
          },
          [] as IIntent[]
        )
        .value();

      return voxaSheetIntent.data;
    })
    .flattenDeep()
    .value();

  return result as IIntent[];
}

export function publishingProcessor(voxaSheets: IVoxaSheet[], AVAILABLE_LOCALES: string[]) {
  const voxaSheetsPublishing = voxaSheets.filter(voxaSheet =>
    _.includes(
      [
        SheetTypes.SKILL_ENVIRONMENTS,
        SheetTypes.SKILL_LOCALE_INFORMATION,
        SheetTypes.SKILL_GENERAL_INFORMATION
      ],
      getSheetType(voxaSheet)
    )
  );

  return voxaSheetsPublishing.reduce(
    (acc, voxaSheet: IVoxaSheet) => {
      voxaSheet.data.map((item: any) => {
        const locale = voxaSheet.sheetTitle.split("@")[1] || AVAILABLE_LOCALES[0];
        const environments = _.chain(item)
          .get("environment", "")
          .split(",")
          .map(_.trim)
          .compact()
          .value() as string[];
        const key = _.chain(item)
          .get("key", "")
          .replace("{locale}", locale)
          .value();
        let value = _.chain(item)
          .get("value", "")
          .value();

        const containsParseNumberKey = ["maxWidth", "minWidth", "maxHeight", "minHeight"].some(s =>
          key.includes(s)
        );

        if (containsParseNumberKey && _.toNumber(value)) {
          value = _.toNumber(value);
        }

        const publishInfo: IPublishingInformation = { key, value, environments };
        acc.push(publishInfo);
      });

      return acc;
    },
    [] as IPublishingInformation[]
  );
}

export function filterSheets(voxaSheets: IVoxaSheet[], sheetTypes: string[]): IVoxaSheet[] {
  return _.filter(voxaSheets, voxaSheet => _.includes(sheetTypes, getSheetType(voxaSheet)));
}

function reduceIntent(propName: string) {
  return (acc: any[], row: any) => {
    row.data = _.chain(row.data)
      .reduce((accData: {}[], item: any) => {
        _.map(item, (value, key) => {
          const obj: any = { intent: key };
          obj[propName] = value;
          accData.push(obj);
        });
        return accData;
      }, [])
      .groupBy("intent")
      .value();

    acc.push(row);
    return acc;
  };
}

function splitValues(head: any, propertyName: string, shouldLowerCase?: boolean): string[] {
  let splittedValue = (_.chain(head) as any)
    .get(propertyName, "")
    .split(",")
    .map(_.trim);

  if (shouldLowerCase) {
    splittedValue = splittedValue.map(_.toLower);
  }

  return splittedValue.compact().value() as string[];
}

function getIntentValueList(
  voxaSheets: IVoxaSheet[],
  spreadsheetId: string,
  intentName: string,
  key: string
): string[] {
  return _(voxaSheets)
    .filter(sheet => sheet.spreadsheetId === spreadsheetId)
    .map((spreadSheet: IVoxaSheet) => spreadSheet.data[intentName] || [])
    .flatten()
    .map(key)
    .compact()
    .uniq()
    .value();
}