Skip to content
This repository has been archived by the owner on Nov 16, 2023. It is now read-only.

Commit

Permalink
feat: update bot memory for imported TrainDialog rounds containing de…
Browse files Browse the repository at this point in the history
…terministic API calls (#1354)

* feat: wip, not working

* feat: set memory state after api calls

* chore: comments

* refactor: unroll nesting

* refactor: increase timeout on training wait

* refactor: revert timeout change

* refactor: unroll fn using map

* fix: update rounds
  • Loading branch information
tpmsr authored Oct 28, 2019
1 parent 97c7834 commit 61b3791
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 50 deletions.
8 changes: 4 additions & 4 deletions src/Utils/obiDialogParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,6 @@ export class ObiDialogParser {
private async collectDialogNodes(obiDialog: OBITypes.OBIDialog, conditionalEntities: { [key: string]: Set<string> }):
Promise<ObiDialogNode> {
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)
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
Expand All @@ -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]
}
}
Expand Down
199 changes: 154 additions & 45 deletions src/Utils/obiUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -533,64 +543,163 @@ export async function createImportedActions(
scorerStepConditions: { [key: string]: CLM.Condition[] } | undefined,
createActionThunkAsync: (appId: string, action: CLM.ActionBase) => Promise<CLM.ActionBase | null>,
): Promise<void> {

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.
*
Expand Down
11 changes: 10 additions & 1 deletion src/routes/Apps/App/TrainDialogs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1278,7 +1278,16 @@ class TrainDialogs extends React.Component<Props, ComponentState> {
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(
Expand Down

0 comments on commit 61b3791

Please sign in to comment.