UNPKG

voxa-cli

Version:
470 lines (418 loc) 15.3 kB
/* * 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(); }