UNPKG

ask-cli-x

Version:

Alexa Skills Kit (ASK) Command Line Interfaces

706 lines (705 loc) 42.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.DialogEvaluateController = exports.DIALOG_STATE_MACHINE = exports.CLI_GEN_NAMESPACE = void 0; const acdl_1 = require("@alexa/acdl"); const fs_extra_1 = require("fs-extra"); const path_1 = __importDefault(require("path")); const smapi_client_1 = require("../../clients/smapi-client"); const cli_error_1 = __importDefault(require("../../exceptions/cli-error")); const cli_retriable_error_1 = require("../../exceptions/cli-retriable-error"); const inputs_1 = require("../../model/dialog/inputs"); const json_view_1 = require("../../view/json-view"); const skill_simulation_controller_1 = require("../skill-simulation-controller"); const acdl_line_parser_1 = require("./utils/acdl-line-parser"); const map_expressions_1 = require("./utils/map-expressions"); const expression_matcher_1 = require("./utils/expression-matcher"); const simulation_response_1 = require("./types/simulation-response"); const simulation_response_parser_1 = require("./simulation-response-parser"); const turn_cursor_1 = require("./utils/turn-cursor"); const acdl_utils_1 = require("./utils/acdl-utils"); const interactions_1 = require("./types/interactions"); const acdl_interaction_module_factory_1 = __importStar(require("./acdl-interaction-module-factory")); const explain_ast_1 = require("./utils/explain-ast"); const types_1 = require("./types"); exports.CLI_GEN_NAMESPACE = "askcli.interaction"; var DIALOG_STATE_MACHINE; (function (DIALOG_STATE_MACHINE) { DIALOG_STATE_MACHINE[DIALOG_STATE_MACHINE["TURN_START"] = 0] = "TURN_START"; DIALOG_STATE_MACHINE[DIALOG_STATE_MACHINE["CORRECTION_START"] = 1] = "CORRECTION_START"; DIALOG_STATE_MACHINE[DIALOG_STATE_MACHINE["CORRECTION_CONTINUE"] = 2] = "CORRECTION_CONTINUE"; DIALOG_STATE_MACHINE[DIALOG_STATE_MACHINE["CORRECTION_COMPLETE"] = 3] = "CORRECTION_COMPLETE"; DIALOG_STATE_MACHINE[DIALOG_STATE_MACHINE["END"] = 4] = "END"; })(DIALOG_STATE_MACHINE = exports.DIALOG_STATE_MACHINE || (exports.DIALOG_STATE_MACHINE = {})); /** * Controller for the dialog evaluate mode. * The class tracks the state of the dialog turn and correction, with event hooks through the flow. * It tracks the SMAPI interactions and update the states accordingly. * * The control flow and the event handler looks like below: * ``` * * (loop) - TURN_START // turnView starts * USER > + onUtteranceInput -> this.simulateUtterance * ALEXA> * Response accept? + onTurnConfirm -> this.passTurn * (loop) - CORRECTION_START // correctionView starts * ------------------- * In prediction mode: * prediction: * acdl.printNode -> this.generateAcdl * accept?: + onPredictionConfirm -> this.passPrediction * correction: + onCorrectionInput -> this.correctTurn * (loop end) * * Goodbye! - END // loop of turns finish and dialog quits * ``` */ class DialogEvaluateController extends skill_simulation_controller_1.SkillSimulationController { /** * Constructor for DialogEvaluateController. * @param {Object} props | config object includes information such as skillId, locale, profile, stage. */ constructor(props) { super(props); const project = props.project || (() => { const projectConfig = (0, acdl_1.loadProjectConfigSync)(process.cwd(), this.profile); return (0, acdl_1.loadProjectSync)(projectConfig); })(); this.initialNewSession = props.newSession === false ? props.newSession : true; this.timeStamp = this._getFormattedTimeStamp(); this.interactionFilePath = path_1.default.join(process.cwd(), "skill-package", "conversations", `cli-gen-interaction-${this.timeStamp}.acdl`); this.namespace = `${exports.CLI_GEN_NAMESPACE}_${this.timeStamp}`; this.moduleFactory = (0, acdl_interaction_module_factory_1.default)(project, this.timeStamp, this.namespace); } /** * Call simulate-skill to get service prediction over a user utterance. There are two cases this method is called: * 1) Call at the start of each dialog turn, to get the initial prediction from the service, and build it into ACDL and display * 2) Call after a correction is requested, to refresh the this.turnSimulation, with the latest service response after a prediction gets corrected * The simulation response is loaded to its POJO during the flow. * * @param utterance */ async simulateUtterance(evaluateContext, utterance, newSession) { var _a, _b, _c, _d, _e, _f, _g; const postSimulationResponse = await this.startSkillSimulation(utterance, newSession); const simulationResult = await this.getSkillSimulationResult((_a = postSimulationResponse.body) === null || _a === void 0 ? void 0 : _a.id); const turnSimulation = (0, simulation_response_1.transformSimulationResponse)(simulationResult.body); const responses = turnSimulation.alexaResponses.map((ar) => ar.content.caption); const firstResponse = turnSimulation.alexaResponses.length > 0 ? turnSimulation.alexaResponses[0].content.caption : undefined; const lastConversation = (_b = turnSimulation.conversations) === null || _b === void 0 ? void 0 : _b[((_c = turnSimulation.conversations) === null || _c === void 0 ? void 0 : _c.length) - 1]; const conversationPredictionAst = lastConversation && this.buildInitialPredictions(evaluateContext, lastConversation, firstResponse); return { alexaResponses: responses, conversation: conversationPredictionAst, utterance: (_g = (_f = (_e = (_d = turnSimulation === null || turnSimulation === void 0 ? void 0 : turnSimulation.conversations) === null || _d === void 0 ? void 0 : _d[0]) === null || _e === void 0 ? void 0 : _e.event) === null || _f === void 0 ? void 0 : _f.input) !== null && _g !== void 0 ? _g : utterance, delegationType: turnSimulation.type, }; } /** * An empty actions array indicates the Action has failed even if getSimulation response has successful status code. * * @param simulationResult response from getSimulation * @returns boolean representing whether any conversations object has an empty actions array */ actionFailed(conversation) { return conversation && !conversation.actions.length; } /** * Based on the service prediction, typeDecl or the content will be passed to this.draftInitialBuilder and get compiled. * The compiled AST will then be used to get printed to ACDL. Result of AST and ACDL will be returned for the View to display. * * @returns ACDLLineInfo */ buildInitialPredictions(context, lastConversation, firstResponse) { const [[event, ...actions]] = [lastConversation.event, ...lastConversation.actions].reduce(([interactions, originInteractions], prediction) => { const originNodes = this.moduleFactory.getAllContentInPredictionAsts(originInteractions); // Generate AST based on the current state. const sourceScope = this.moduleFactory.contextToProjectScope(context, originInteractions); // Expression map from only most recent server prediction const serverPredictionSimpleExpressionMap = (0, map_expressions_1.simpleExpressionMap)(originNodes, sourceScope.project); const originInteraction = this.simulationResponseToAST(prediction, serverPredictionSimpleExpressionMap, sourceScope.project, sourceScope.module, new simulation_response_parser_1.Counter(context.interactionBlock.length.toString()), firstResponse); /** * Some types might be missing if we dont include the current source node in the project. */ const sourceScope2 = this.moduleFactory.contextToProjectScope(context, [...originInteractions, originInteraction]); return [ [ ...interactions, (currentInteractions, counter) => { const { project: targetProject, module: targetModule } = this.moduleFactory.contextToProjectScope(context, currentInteractions); const currentNodes = currentInteractions && this.moduleFactory.getAllContentInPredictionAsts(currentInteractions); const _counter = counter !== null && counter !== void 0 ? counter : new simulation_response_parser_1.Counter(context.interactionBlock.length.toString()); /** * A mapping between expression outputs and types used by expressions used to bridge the current * interaction state and the one returned by future server responses. * * If no current interaction group is given, use the source one in the closure only. */ const expressionMap = currentNodes ? (0, map_expressions_1.mapExpressions)(originNodes, currentNodes, sourceScope2.project, targetProject) : (0, map_expressions_1.simpleExpressionMap)(originNodes, sourceScope2.project); return this.simulationResponseToAST(prediction, expressionMap, targetProject, targetModule, _counter, firstResponse); }, ], [...originInteractions, originInteraction], ]; }, [[], []]); return { event, actions }; } /** * This handler is triggered when user input Yes after the initial overview of the ACDL prediction is displayed. * Based on user's decision: * 1) Place the approved conversation into the greater evaluation context interaction block. */ acceptTurn(turnContext) { const counter = new simulation_response_parser_1.Counter(turnContext.interactionBlock.length.toString()); return { ...turnContext, interactionBlock: turnContext.last.conversation ? [ ...turnContext.interactionBlock, { // We don't fully resolve the uncommitted interactions until the last minute. // This is when we do it for accepted turns. expressions: this.moduleFactory.resolveTurnInteractions([turnContext.last.conversation.event, ...turnContext.last.conversation.actions], counter), }, ] : turnContext.interactionBlock, }; } /** * This handler is triggered when user input No after the initial overview of the ACDL prediction is displayed. * Based on user's decision: * 1) Correction mode starts, all the state values inside correction mode reset. */ async rejectTurn(dialogContext) { // These properties should not be null at this point, but lets check anyways. // The DX should not allow correction when there is no conversation data. const lastConversation = dialogContext.last.conversation; if (!lastConversation) { throw new cli_error_1.default("Cannot reject or correct a non-conversation response."); } return { ...dialogContext, turnCorrections: { endPrediction: false, corrections: [] }, turnCursor: { eventPrediction: lastConversation.event, }, last: { ...dialogContext.last, conversation: lastConversation, }, // initialize the turn interaction group. We have no corrections yet, so maintain one array. turnInteractionGroup: { expressions: [], }, }; } /** * This handler is called right before each prediction is going to be displayed. * Based on this.turnCorrectionStarted, this.acdlbuilder or this.draftBuilderInCorrection will be used to execute the build/compile. * The AST will again be transformed to ACDL and used for display. */ turnToAcdl(correctionContext) { if ((0, turn_cursor_1.isEndTurnCursor)(correctionContext.turnCursor)) { return [simulation_response_1.EndTurnPrediction]; } const ast = (0, turn_cursor_1.predictionAtCursor)(correctionContext.turnCursor); const resolvedTurn = this.moduleFactory.resolveTurnInteractionForContext(correctionContext, ast); const { project } = this.moduleFactory.contextToProjectScope(correctionContext, [resolvedTurn]); return [ ...(resolvedTurn.ast.typeDecl ? [this.moduleFactory.printAstToAcdl(resolvedTurn.ast.typeDecl, project)] : []), this.moduleFactory.printAstToAcdl(resolvedTurn.ast.content, project), ]; } shouldDisableTurnCorrection(correctionContext) { return (0, turn_cursor_1.isEventTurnCursor)(correctionContext.turnCursor) && correctionContext.last.delegationType === simulation_response_1.DelegationType.Ingress; } updateCorrectionPayload(correctionPayload, project, ast) { return { ...correctionPayload, corrections: [ ...correctionPayload.corrections, ...(ast.typeDecl ? [this.moduleFactory.astToAcir(project, ast.typeDecl)] : []), ...(correctionPayload.endPrediction ? [] : [this.moduleFactory.astToAcir(project, ast.content)]), ], }; } /** * Handler when user approves a single prediction. * When user approves a prediction: * 1) If the turnCursor shows this is already end of the turn, correction mode finishes * 2) Otherwise the approved prediction will be built and compiled into either this.acdlBuilder or this.draftBuilderInCorrection. * * @returns complete */ acceptPrediction(correctionContext) { var _a; if (this.actionFailed((_a = correctionContext === null || correctionContext === void 0 ? void 0 : correctionContext.last) === null || _a === void 0 ? void 0 : _a.conversation)) { throw new cli_retriable_error_1.CliRetriableError("Something went wrong. Check your skill's Lambda response."); } /* end of turn, all predictions have been corrected/approved within this turn */ // TODO: Should we ask about end turn? if ((0, turn_cursor_1.isEndTurnCursor)(correctionContext.turnCursor)) { // All done, nothing else to do, but commit. We'll commit in the correction end state. return [true, correctionContext]; } else { // not complete const currentTurnAst = (0, turn_cursor_1.predictionAtCursor)(correctionContext.turnCursor); // Resolve the AST of the current turn before generating ACDL for the corrections array. const contextWithCorrections = { ...correctionContext, // if we are already correcting, do nothing. turnInteractionGroup: (0, interactions_1.isUnresolvedExpressionInteractionGroup)(correctionContext.turnInteractionGroup) ? { expressions: [...correctionContext.turnInteractionGroup.expressions, currentTurnAst] } : // If we are just starting to correct. { actual: correctionContext.turnInteractionGroup.actual, expected: [...correctionContext.turnInteractionGroup.expected, currentTurnAst], }, }; const projectScope = this.moduleFactory.contextToProjectScope(contextWithCorrections); const lastTurn = (0, acdl_interaction_module_factory_1.getLastExpectedFromInteractionGroup)(projectScope.lastGroup); if (!lastTurn) { throw new cli_error_1.default("Missing last turn."); } return [ false, { ...contextWithCorrections, // collect actuals for future possible modifyLastTurn call, and move to next cursor when prediction passes turnCorrections: this.updateCorrectionPayload(contextWithCorrections.turnCorrections, projectScope.project, lastTurn.ast), turnCursor: (0, turn_cursor_1.createOrIncrementTurnCursor)(contextWithCorrections.last.conversation, contextWithCorrections.turnCursor), }, ]; } } rejectPrediction(correctionContext) { // The interaction group is building a single list of interaction for the turn, change it into a correction group if ((0, interactions_1.isUnresolvedExpressionInteractionGroup)(correctionContext.turnInteractionGroup)) { return { ...correctionContext, turnInteractionGroup: { // We resolve the interactions here because we will no longer edit any of the context above this point // (except for types) actual: this.moduleFactory.resolveTurnInteractions([correctionContext.last.conversation.event, ...correctionContext.last.conversation.actions], // This is the last time we will resolve these nodes. Resolve with unique generated names. new simulation_response_parser_1.Counter(`ACTUAL_${correctionContext.interactionBlock.length}`)), // Expected is all approved turns up to here. expected: correctionContext.turnInteractionGroup.expressions, }, }; } // If rejected, do nothing. return correctionContext; } /** Commits the result of the correction into the evaluate context and returns it */ commitCorrection(correctionContext) { const counter = new simulation_response_parser_1.Counter(correctionContext.interactionBlock.length.toString()); return { interactionBlock: [ ...correctionContext.interactionBlock, this.moduleFactory.resolveInteractionGroup(correctionContext.turnInteractionGroup, counter), ], }; } // TODO: Can we move most of the validation to the validateTurn method? async getCorrectionTurn(correctionContext, corrections) { const { type, content } = corrections; if (!(type === null || type === void 0 ? void 0 : type.correction) && !content.correction) { throw new cli_retriable_error_1.CliRetriableError(`Inputs are empty, please try again.`); } const correctedPrediction = !(0, turn_cursor_1.isEndTurnCursor)(correctionContext.turnCursor) ? this.moduleFactory.resolveTurnInteractionForContext(correctionContext, (0, turn_cursor_1.predictionAtCursor)(correctionContext.turnCursor)) : undefined; const typedeclAst = (type === null || type === void 0 ? void 0 : type.correction) ? this.validateAndParseCorrectionType(type.correction, correctionContext) : // If the type isn't given, try to assign the type which was previously there. correctedPrediction === null || correctedPrediction === void 0 ? void 0 : correctedPrediction.ast.typeDecl; const contentAst = this.validateAndParseCorrectionContent(content.correction, correctedPrediction === null || correctedPrediction === void 0 ? void 0 : correctedPrediction.ast.content); // Does this need to be in the closure? Could we compute and then put in the closure? This may be useful later... return () => { var _a; return { // Clone because adding to the interaction later will mutate them. ast: { typeDecl: typedeclAst === null || typedeclAst === void 0 ? void 0 : typedeclAst.clone(), content: contentAst.clone() }, // Assume the correction is the same type source: (_a = correctedPrediction === null || correctedPrediction === void 0 ? void 0 : correctedPrediction.source) !== null && _a !== void 0 ? _a : "action", }; }; } validateCorrection(project, lastGroup) { const lastTurn = (0, acdl_interaction_module_factory_1.getLastExpectedFromInteractionGroup)(lastGroup); if (!lastTurn) { throw new cli_retriable_error_1.CliRetriableError("Missing last turn."); } this.validateTurn(project, lastTurn); return lastTurn.ast; } validateTurn(project, turnInteraction) { const typeLastTurn = turnInteraction.ast.typeDecl; const contentLastTurn = turnInteraction.ast.content; const checker = project.getTypeChecker(); const typeErrors = typeLastTurn && (0, acdl_1.validateTypeDeclaration)(typeLastTurn, checker); if (typeErrors && typeErrors.length > 0) { throw new cli_retriable_error_1.CliRetriableError("Invalid ACDL type input. Errors: " + typeErrors.map((e) => e.message).join(",")); } const contentErrors = (0, acdl_1.validateExpression)(contentLastTurn, checker); if (contentErrors && contentErrors.length > 0) { throw new cli_retriable_error_1.CliRetriableError("Invalid ACDL content input. Errors: " + contentErrors .map((e) => e.message) .map((message) => { const arr = message.split(","); return arr.filter((value) => !value.includes(this.namespace)).join(","); }) .join(",")); } } /** * Handler when user provide the corrections (two lines but typeDecl can be optional). * The user input ACDL lines will be compiled to AST and builder into this.acdlBuilder. * Then CLI makes modifyLastTurn API call with this.turnCorrectionPayload, and following with a refresh for this.turnSimulation. * After the correction is succeeded, we calibrate the turnCursor based on the prediction, and update existing variables. * * @param correctedAcdl user input for corrections */ async updateCorrectionContextWithCorrections(correctionContext, corrections) { const isEndTurn = (0, inputs_1.isCommand)(corrections); const updatedCorrectionContext = { ...correctionContext, turnCorrections: { endPrediction: isEndTurn, corrections: correctionContext.turnCorrections.corrections, }, }; const unresolvedCorrectionTurn = isEndTurn ? [] : [await this.getCorrectionTurn(updatedCorrectionContext, corrections)]; if ((0, interactions_1.isUnresolvedExpressionInteractionGroup)(updatedCorrectionContext.turnInteractionGroup)) { throw new cli_error_1.default("Unexpected expression interaction group during correction."); } const contextWithCorrections = { ...updatedCorrectionContext, turnInteractionGroup: { actual: updatedCorrectionContext.turnInteractionGroup.actual, expected: [...updatedCorrectionContext.turnInteractionGroup.expected, ...unresolvedCorrectionTurn], }, }; const { project, module, lastGroup } = this.moduleFactory.contextToProjectScope(contextWithCorrections); const [validatedTurn, updatedCorrectionPayload] = this.updateCorrectionPayloadWithValidatedTurn(contextWithCorrections, project, lastGroup, isEndTurn); const updatedCorrectionPayloadWithTypes = this.updateCorrectionPayloadWithResolvedTypes(updatedCorrectionPayload, validatedTurn === null || validatedTurn === void 0 ? void 0 : validatedTurn.content, project, module); try { await this.modifyLastTurn(updatedCorrectionPayloadWithTypes); // refresh the prediction sequence after modifyLastTurn const nextConversation = await this.simulateUtterance(contextWithCorrections, contextWithCorrections.last.utterance, false); if (this.actionFailed(nextConversation.conversation)) { throw new cli_retriable_error_1.CliRetriableError("Something went wrong. Check your skill's Lambda response."); } if (!(0, types_1.isCorrectionTurnResult)(nextConversation)) { throw new Error("Something went wrong. The simulation must be contain a conversation."); } // Pass end turn when the input is EndTurn command and not a correction object. const updatedTurnCursor = isEndTurn ? turn_cursor_1.END_TURN_CURSOR : this.updateCursor(contextWithCorrections, project, nextConversation, validatedTurn === null || validatedTurn === void 0 ? void 0 : validatedTurn.content); return { ...contextWithCorrections, last: nextConversation, turnCorrections: updatedCorrectionPayloadWithTypes, turnCursor: updatedTurnCursor, }; } catch (e) { if (e instanceof cli_retriable_error_1.RetriableServiceError) { throw new cli_retriable_error_1.RetriableServiceError(`${e.message}\nPlease try again for the correction.`); } else { throw e; } } } updateCorrectionPayloadWithResolvedTypes(correctionPayload, correctionContent, project, module) { var _a; if (!correctionContent) return correctionPayload; const genericArguments = (0, acdl_1.isCall)(correctionContent) ? correctionContent.genericArguments : (_a = correctionContent.expression) === null || _a === void 0 ? void 0 : _a.genericArguments; const correctionReferences = genericArguments === null || genericArguments === void 0 ? void 0 : genericArguments.filter(acdl_1.isTypeReference).map((genericArgument) => { var _a, _b; const node = ((_a = genericArgument.name) === null || _a === void 0 ? void 0 : _a.name) && (0, simulation_response_parser_1.getLastExpressionFromContext)((_b = genericArgument.name) === null || _b === void 0 ? void 0 : _b.name, project, module); const acir = node && this.moduleFactory.astToAcir(project, node); return acir; }); if (!correctionReferences) return correctionPayload; const corrections = [...correctionPayload.corrections, ...correctionReferences]; return { endPrediction: correctionPayload.endPrediction, corrections: [...new Map(corrections.map((correction) => [correction.name, correction])).values()], }; } updateCorrectionPayloadWithValidatedTurn(contextWithCorrections, project, lastGroup, isEndTurn) { if (isEndTurn) { return [undefined, contextWithCorrections.turnCorrections]; } else { const validatedTurn = this.validateCorrection(project, lastGroup); return [validatedTurn, this.updateCorrectionPayload(contextWithCorrections.turnCorrections, project, validatedTurn)]; } } validateAndParseCorrectionContent(correctionAcdl, correctedContentAst) { var _a; try { const contentAst = (0, acdl_line_parser_1.parseACDLLine)(correctionAcdl, this.namespace); if (!(contentAst instanceof acdl_1.Call || contentAst instanceof acdl_1.NameDeclaration)) { throw new cli_retriable_error_1.CliRetriableError(`Failed to compile the second line, please try again. Input must be based on action call for user turn or response() for Alexa turn.`); } if (contentAst instanceof acdl_1.NameDeclaration) { if (!(0, acdl_utils_1.isNamedCallExpression)(contentAst)) { throw new cli_retriable_error_1.CliRetriableError(`Failed to compile the second line, please try again. Input must be based on action call for user turn or response() for Alexa turn.`); } } else if (contentAst instanceof acdl_1.Call) { if (correctedContentAst && correctedContentAst instanceof acdl_1.NameDeclaration && !(((_a = contentAst.name) === null || _a === void 0 ? void 0 : _a.name) === "response")) { // TODO: Make this more flexible by understanding if the replaced names is not used. throw new cli_retriable_error_1.CliRetriableError("Second line must be named if the corrected line was named."); } } return contentAst; } catch (e) { if (e instanceof cli_retriable_error_1.CliRetriableError) throw e; throw new cli_retriable_error_1.CliRetriableError(`Failed to compile the second line, please try again. ${e}`); } } validateAndParseCorrectionType(correctionAcdl, correctionContext) { try { // Validate type correction... const typedeclAst = (0, acdl_line_parser_1.parseACDLLine)(correctionAcdl, this.namespace); if (!(typedeclAst instanceof acdl_1.TypeDeclaration)) { throw new cli_retriable_error_1.CliRetriableError(`Failed to compile the first line, please try again. Input is optional but must declare a new type.`); } this.validateNewTypeName(typedeclAst, correctionContext); return typedeclAst; } catch (e) { if (e instanceof cli_retriable_error_1.CliRetriableError) throw e; throw new cli_retriable_error_1.CliRetriableError(`Failed to compile the first line, please try again. ${e}`); } } async modifyLastTurn(currentPayload) { try { const res = await this.smapiClient.skill.test.modifyLastTurn({ skillId: this.skillId, stage: this.stage, locale: "en-US", payload: currentPayload, }); if ((0, smapi_client_1.isSmapiError)(res)) { let errMsg = (0, json_view_1.toString)(res.body); if (res.body && res.body.message) { const violations = res.body.violations ? ` with violation ${res.body.violations.map((v) => v.message).join(", ")}` : ""; errMsg = `${res.body.message}${violations}`; } throw new cli_retriable_error_1.RetriableServiceError(`Failed to correct. Service error: ${errMsg}`, res.body); } return res; } catch (err) { if (err instanceof cli_retriable_error_1.RetriableServiceError) { throw err; } throw new cli_retriable_error_1.RetriableServiceError(`Failed to correct. Service error: ${err}`); } } // TODO: change this method to format "command outputs" that are read by the view. // Should encapsulate all needed data, no formatting string. async handleOutputSpecialCmds(evaluateContext, cmd) { switch (cmd) { case inputs_1.SpecialCommand.SAVE: return await this.handleSaveCommand(evaluateContext); case inputs_1.SpecialCommand.VARS: // TOOD: Compute variable names from the context return this.printVarNames(evaluateContext); default: const exhaustive = cmd; return exhaustive; } } async handleSaveCommand(evaluateContext) { await this.writeToFile(evaluateContext); return `On-going interactions saved to ${path_1.default.relative(process.cwd(), this.interactionFilePath)}. Run "askx deploy" to apply any correction to your model.`; } printVarNames(evaluateContext) { var _a, _b; const { project, lastGroup } = this.moduleFactory.contextToProjectScope(evaluateContext); const node = lastGroup ? (_a = (0, acdl_interaction_module_factory_1.getLastExpectedFromInteractionGroup)(lastGroup)) === null || _a === void 0 ? void 0 : _a.ast.content : (_b = (0, acdl_interaction_module_factory_1.getLastExpectedFromInteractionGroup)(evaluateContext.interactionBlock[evaluateContext.interactionBlock.length - 1])) === null || _b === void 0 ? void 0 : _b.ast.content; const vars = (0, acdl_utils_1.getVisibleLocalVariableNames)(project.getTypeChecker(), node); if (vars.length === 0) { return "No variables declared yet."; } else { return `Variables: ${vars.join(", ")}`; } } async writeToFile(evaluationContext) { const { project, module } = this.moduleFactory.contextToProjectScope(evaluationContext); await (0, fs_extra_1.outputFile)(this.interactionFilePath, (0, acdl_1.printModule)(module, project.getTypeChecker())); } async partitionAndFormatAstLines(context, correctionResult, responses) { const counter = new simulation_response_parser_1.Counter(context.interactionBlock.length.toString()); const interactions = this.moduleFactory.resolveTurnInteractions([correctionResult.conversation.event, ...correctionResult.conversation.actions], counter); const { project } = this.moduleFactory.contextToProjectScope(context, interactions); const acdlLines = interactions.map((interaction) => ({ // Before lines are printed before the partitioned responses before: interaction.source === "event", type: interaction.ast.typeDecl && { acdl: this.moduleFactory.printAstToAcdl(interaction.ast.typeDecl, project), explanation: (0, explain_ast_1.explainAst)(interaction.ast.typeDecl), }, content: { acdl: this.moduleFactory.printAstToAcdl(interaction.ast.content, project), explanation: (0, explain_ast_1.explainAst)(interaction.ast.content), }, })); // Flatten the ACDL lines and // then partition between lines to be printed before the responses and ones to be printed with the responses const [beforeLines, adclResponseLines] = acdlLines .map(({ type, content, before }) => ({ lines: [...(type ? [type] : []), content], before })) .reduce(([beforeLines, responses], { lines, before }) => { return [before ? [...beforeLines, ...lines] : beforeLines, !before ? [...responses, ...lines] : responses]; }, [[], []]); // Partition the ACDL lines with each partition ending with response() const partitionedAcdlLines = adclResponseLines .reduce((acc, resp) => { const [head = [], ...rest] = acc; // Add a new partition to the start when the response ends with response() return [ ...(resp.acdl.match(/.*response\(.+\).*/g) ? [[]] : []), // Add the current response to the first partition [...head, resp], ...rest, ]; }, []) // New partitions are at the end of the list, reverse to get the right order .reverse(); return { // Match a response with the partitioned acdlLines. // Its possible for the acdlLine partitions or the responses to be longer. groups: [...Array(Math.max(partitionedAcdlLines.length, responses.length)).keys()].map((i) => ({ responseLine: responses.length > i ? responses[i] : undefined, acdlLines: partitionedAcdlLines.length > i ? partitionedAcdlLines[i] : undefined, })), before: beforeLines, }; } simulationResponseToAST(payload, expressionMap, project, module, counter, firstResponse) { if ((0, simulation_response_1.isEventPrediction)(payload)) { return (0, simulation_response_parser_1.getReceivedAST)(payload, counter, project, module); } else if ((0, simulation_response_1.isActionPrediction)(payload)) { if (!firstResponse) { throw new cli_error_1.default("Response AST expects an Alexa Response."); } return (0, simulation_response_parser_1.getResponseAST)(payload, this.escapeRegExp(firstResponse), project, module, expressionMap, counter); } else { throw new cli_error_1.default(`Convert unexpected data to AST: ${payload}`); } } escapeRegExp(input) { return input.replace(/"/g, "\\$&"); } validateNewTypeName(newTypeDecl, correctionContext) { var _a; if (!((_a = newTypeDecl.name) === null || _a === void 0 ? void 0 : _a.name)) { throw new cli_retriable_error_1.CliRetriableError(`The name for the newly declared type must not be empty.`); } const resolvedGroup = this.moduleFactory.resolveInteractionGroup(correctionContext.turnInteractionGroup, new simulation_response_parser_1.Counter(correctionContext.interactionBlock.length.toString())); // avoid duplicate typeDecl name // TODO: Do this with the parse instead? Probably const dupExist = this.moduleFactory .getAllTypesInInteractionGroups([...correctionContext.interactionBlock, resolvedGroup]) .some((expr) => { var _a, _b; return ((_a = expr.name) === null || _a === void 0 ? void 0 : _a.name) === ((_b = newTypeDecl.name) === null || _b === void 0 ? void 0 : _b.name); }); if (dupExist) { throw new cli_retriable_error_1.CliRetriableError(`The name for the newly declared type "${(0, acdl_1.getName)(newTypeDecl.name.name)}" is already used, please use another name for your type.`); } } /** * Calibrate the turnCursor after a correction happens, i.e. try to find the current position with the refreshed simulation response. * The source of truth is the last correction user made, which must appear in the refreshed simulation response. * This function iterates through the refreshed this.turnSimulation to find the lastCorrectedAST. Once found, update the turnCursor. */ updateCursor(context, project, nextTurn, expectedAst) { var _a, _b; try { // the last expression in correction.expected should be ask.Call const lastCorrectedAst = expectedAst instanceof acdl_1.NameDeclaration ? expectedAst.expression : expectedAst; // this shouldn't happen... if (!lastCorrectedAst) { throw new cli_error_1.default("Content correction should have an expression."); } // When the last corrected AST is an event, set the cursor to the first action. if ((_b = (_a = lastCorrectedAst.name) === null || _a === void 0 ? void 0 : _a.name) === null || _b === void 0 ? void 0 : _b.includes("received")) { return (0, turn_cursor_1.turnCursorFromIndex)(nextTurn.conversation, 0); } // When the last corrected AST is a response, try to find that response in the server response else { const matcher = (0, expression_matcher_1.getCallMatcher)(lastCorrectedAst, project); const [module, group] = this.moduleFactory.contextPlusUnresolvedToModule(context, [ nextTurn.conversation.event, ...nextTurn.conversation.actions, ]); // group will be present, because we gave it. const newProject = this.moduleFactory.getProjectWithModule(module); // Index will already be one higher than the action list because the resolved interactions include the event. const candidateIndex = ((0, interactions_1.isExpressionInteractionGroup)(group) ? group.expressions : group.expected) .map((x) => x.ast.content) .findIndex((c) => matcher(c, newProject)); if (candidateIndex > nextTurn.conversation.actions.length) { throw new cli_error_1.default(`Unexpected service error. The last correction is not showing in the latest service predictions.`); } // all checkpoints passed, we find the lastCorrectedAst and update the turnCursor return (0, turn_cursor_1.turnCursorFromIndex)(nextTurn.conversation, candidateIndex !== -1 ? candidateIndex : nextTurn.conversation.actions.length - 1); } } catch (e) { throw new cli_error_1.default(`Unexpected error when calibrate turn cursor: ${e}`); } } _getFormattedTimeStamp() { return new Date().toISOString().replace(/[:|\-|.]/g, "_"); } } exports.DialogEvaluateController = DialogEvaluateController;