diff --git a/src/Utils/obiDialogParser.ts b/src/Utils/obiDialogParser.ts index c65c39dd..8f207526 100644 --- a/src/Utils/obiDialogParser.ts +++ b/src/Utils/obiDialogParser.ts @@ -143,7 +143,6 @@ export class ObiDialogParser { private async collectDialogNodes(obiDialog: OBITypes.OBIDialog, conditionalEntities: { [key: string]: Set }): Promise { let node: ObiDialogNode = new ObiDialogNode(obiDialog) - // TODO(thpar) : add steps for capturing API input and output. if (obiDialog.rules) { await this.collectDialogRuleChildren(node, obiDialog.rules, conditionalEntities) } @@ -303,7 +302,7 @@ export class ObiDialogParser { currentRequiredCondition = node.requiredCondition } - let rounds = [...currentRounds] + let rounds = Util.deepCopy(currentRounds) // Build up a training round from any applicable steps in this node. const obiDialog = node.dialog if (obiDialog.steps) { @@ -322,6 +321,7 @@ export class ObiDialogParser { // Reset the required condition, since we've now used it. currentRequiredCondition = undefined } + if (currentIntent) { const extractorStep: CLM.TrainExtractorStep = { textVariations: this.getTextVariations(currentIntent) @@ -335,10 +335,10 @@ export class ObiDialogParser { } else { // If we get here, then the current node has steps to execute *without* an intervening intent // (user utterance). We therefore must append these scorer steps to the previous round. - if (currentRounds.length === 0) { + if (rounds.length === 0) { throw new Error(`Attempting to append scorer steps to a non-existent round in node ${obiDialog.$id}`) } - let round = currentRounds[currentRounds.length - 1] + let round = rounds[rounds.length - 1] round.scorerSteps = [...round.scorerSteps, ...scorerSteps] } } diff --git a/src/Utils/obiUtils.tsx b/src/Utils/obiUtils.tsx index 24e467a2..2f33fdb5 100644 --- a/src/Utils/obiUtils.tsx +++ b/src/Utils/obiUtils.tsx @@ -515,6 +515,16 @@ function findActionFromScorerStep(scorerStep: CLM.TrainScorerStep, actions: CLM. return undefined } +/** + * Holds information about enum entities that should be set in a TrainDialog after a round containing + * an API action with known output values. + */ +interface EnumDataFromCondition { + enumEntityId: string + enumValueId: string + enumValueText: string +} + /** * Creates actions for the input `TrainDialog`. * Imports happen in 2 stages : in the first, TrainDialogs are created with placeholder actions that @@ -533,64 +543,163 @@ export async function createImportedActions( scorerStepConditions: { [key: string]: CLM.Condition[] } | undefined, createActionThunkAsync: (appId: string, action: CLM.ActionBase) => Promise, ): Promise { - const newActions: CLM.ActionBase[] = [] for (const round of trainDialog.rounds) { - for (let scoreIndex = 0; scoreIndex < round.scorerSteps.length; scoreIndex = scoreIndex + 1) { - const scorerStep = round.scorerSteps[scoreIndex] - - if (scorerStep.importText) { - let action: CLM.ActionBase | undefined - - // First check to see if matching action already exists - action = findActionFromScorerStep(scorerStep, [...newActions, ...actions], []) - - // Otherwise create a new one - if (!action) { - const isTerminal = round.scorerSteps.length === scoreIndex + 1 - let importedAction: ImportedAction | undefined - if (lgItems) { - const lgName = lgNameFromImportText(scorerStep.importText) - if (lgName) { - let lgItem = lgItems.find(lg => lg.lgName === lgName) - if (lgItem) { - importedAction = { - text: lgItem.text, - buttons: lgItem.suggestions, - isTerminal, - reprompt: lgItem.suggestions.length > 0, - lgName - } - } - else { - // LARS thow error once CCI .dialog transformer has been fixed - lgItem = { lgName: "", text: "Can't Parse LG", suggestions: [] } - //throw new Error(`LG name ${prompt} undefined`) + for (const [scoreIndex, scorerStep] of round.scorerSteps.entries()) { + if (!scorerStep.importText) { + continue + } + // First check to see if matching action already exists. + let action = findActionFromScorerStep(scorerStep, [...newActions, ...actions], []) + + // If not, create a new one. + if (!action) { + const isTerminal = round.scorerSteps.length === scoreIndex + 1 + let importedAction: ImportedAction | undefined + if (lgItems) { + const lgName = lgNameFromImportText(scorerStep.importText) + if (lgName) { + let lgItem = lgItems.find(lg => lg.lgName === lgName) + if (lgItem) { + importedAction = { + text: lgItem.text, + buttons: lgItem.suggestions, + isTerminal, + reprompt: lgItem.suggestions.length > 0, + lgName } } - } - if (!importedAction) { - importedAction = { - text: scorerStep.importText, - buttons: [], - isTerminal, - reprompt: false, - actionHash: CLM.hashText(scorerStep.importText) + else { + // LARS thow error once CCI .dialog transformer has been fixed + lgItem = { lgName: "", text: "Can't Parse LG", suggestions: [] } + //throw new Error(`LG name ${prompt} undefined`) } } - - action = await createActionFromImport(appId, importedAction, templates, scorerStep, scorerStepConditions, createActionThunkAsync) - newActions.push(action) } + if (!importedAction) { + importedAction = { + text: scorerStep.importText, + buttons: [], + isTerminal, + reprompt: false, + actionHash: CLM.hashText(scorerStep.importText) + } + } + action = await createActionFromImport(appId, importedAction, templates, scorerStep, scorerStepConditions, createActionThunkAsync) + newActions.push(action) + } - // Update scorer step - scorerStep.labelAction = action.actionId - delete scorerStep.importText + // Update scorer step + scorerStep.labelAction = action.actionId + delete scorerStep.importText + } + } +} + +/** + * Updates memory state by setting `logicResult` in the `TrainDialog` `TrainScorerSteps` for cases where + * the dialog produces a deterministic output from an API call. + */ +export function setMemoryStateForImportedTrainDialog( + entities: CLM.EntityBase[], + actions: CLM.ActionBase[], + trainDialog: CLM.TrainDialog, + scorerStepConditions: { [key: string]: CLM.Condition[] } | undefined) { + if (!scorerStepConditions) { + // If this is undefined, then there were no SwithCondition nodes in our imported dialog, so nothing to do. + return + } + for (const round of trainDialog.rounds) { + setMemoryStateForApiActionWithSwitch(entities, actions, round.scorerSteps, scorerStepConditions) + } +} + +/** + * Updates memory state for a single `TrainRound`. Specifically, if we have an API action followed by + * a `SwitchCondition` node that consumes the output of that API, we know that each branch of the dialog + * should represent a scenario where the API has returned the given conditional value. This code sets + * that conditional value in bot memory. + */ +function setMemoryStateForApiActionWithSwitch( + entities: CLM.EntityBase[], + actions: CLM.ActionBase[], + scorerSteps: CLM.TrainScorerStep[], + scorerStepConditions: { [key: string]: CLM.Condition[] }) { + for (let i = 0; i < scorerSteps.length - 1; i = i + 1) { + if (!scorerSteps[i].labelAction) { + // Need a labelAction to figure out which action is associated with the scorer step. + continue + } + const j = i + 1 + if (j >= scorerSteps.length) { + // There is no next action; we're done. + break + } + const actionId1 = scorerSteps[i].labelAction + const action1 = actions.find(a => a.actionId === actionId1) + if (!action1 || action1.actionType !== CLM.ActionTypes.API_LOCAL) { + // We only care about API actions followed by SwitchCondition-gated nodes. + continue + } + const scorerStep = scorerSteps[i] + const nextScorerStep = scorerSteps[j] + if (!nextScorerStep.importId || !scorerStepConditions[nextScorerStep.importId]) { + // The next action is not a SwitchCondition-gated node. + continue + } + // Set the logic result on the *current* scorer step. + if (!scorerStep.logicResult) { + scorerStep.logicResult = { logicValue: undefined, changedFilledEntities: [] } + } + // Get conditions from the SwitchCondition-gated node. + const conditions = scorerStepConditions[nextScorerStep.importId] + const enumConditionData = conditions.map(condition => getEnumConditionData(entities, condition)) + for (const conditionEntity of enumConditionData) { + const filledEntity: CLM.FilledEntity = { + entityId: conditionEntity.enumEntityId, + values: [{ + userText: conditionEntity.enumValueText, + displayText: conditionEntity.enumValueText, + builtinType: null, + resolution: null, + enumValueId: conditionEntity.enumValueId + }] } + scorerStep.logicResult.changedFilledEntities.push(filledEntity) } } } +/** + * Gets the enum entity and enum value associated with the given conditions. + * @throws if the entity referenced in any `Condition` is not a valid enum. + */ +function getEnumConditionData(entities: CLM.EntityBase[], condition: CLM.Condition): EnumDataFromCondition { + if (!condition.valueId) { + // This should not happen; conditions created from conditions in .dialog import should always reference enum entities. + throw new Error(`Action condition doesn't reference an entity`) + } + // Find the entity. + const conditionEntity = entities.find(e => e.entityId === condition.entityId) + if (!conditionEntity) { + throw new Error(`Couldn't find entity with id ${condition.entityId}`) + } + if (conditionEntity.entityType !== CLM.EntityType.ENUM || !conditionEntity.enumValues) { + // This should not happen; entities created from conditions in .dialog import should always be enum. + throw new Error(`Entity ${conditionEntity.entityId} is not a valid enum`) + } + // Find the specific enum value referenced by the condition. + const enumValue = conditionEntity.enumValues.find(val => val.enumValueId === condition.valueId) + if (!enumValue) { + throw new Error(`Enum entity ${conditionEntity.entityName} missing enum value ${condition.valueId}`) + } + return { + enumEntityId: condition.entityId, + enumValueId: condition.valueId, + enumValueText: enumValue.enumValue, + } +} + /** * Creates a real action for an `ImportedAction` action placeholder. * diff --git a/src/routes/Apps/App/TrainDialogs.tsx b/src/routes/Apps/App/TrainDialogs.tsx index f913134a..c2765910 100644 --- a/src/routes/Apps/App/TrainDialogs.tsx +++ b/src/routes/Apps/App/TrainDialogs.tsx @@ -1278,7 +1278,16 @@ class TrainDialogs extends React.Component { importData.lgItems, this.props.actions, importData.conditions, - this.props.createActionThunkAsync as any) + this.props.createActionThunkAsync as any, + ) + + // Update memory state, if applicable. + OBIUtils.setMemoryStateForImportedTrainDialog( + this.props.entities, + this.props.actions, + newTrainDialog, + importData.conditions, + ) // Replay to validate newTrainDialog = await DialogEditing.onReplayTrainDialog(