import { Action , Store , Dispatch } from 'redux' ; import { Slot , OutputMessageItem , OutputMessageType , MessageData , ValidateResult , ConvoState , Ability , WolfState , SlotId , SlotConfirmationFunctions , SetSlotDataFunctions , Entity , PromptSlotReason } from '../types' ; import { addMessage , fillSlot as fillSlotAction , startFillSlotStage , setFocusedAbility , removeSlotFromPromptedStack , acceptSlot , denySlot , abilityCompleted , enableSlot , disableSlot , addSlotToPromptedStack , reqConfirmSlot , incrementTurnCountBySlotId , setSlotPrompted , setSlotDone , removeSlotFromOnFillStack } from '../actions' ; import { getPromptedSlotId , isPromptStatus , isFocusedAbilitySet , getSlotBySlotId , getSlotTurnCount , getTargetAbility , getRequestingSlotIdFromCurrentSlotId , getMessageData , getFocusedAbility , getDefaultAbility , getRunOnFillStack , getfilledSlotsOnCurrentTurn } from '../selectors' ; import { findSlotInAbilitiesBySlotId } from '../helpers' ; const logState = require ( 'debug' ) ( 'wolf:s2:enterState' ) ; const log = require ( 'debug' ) ( 'wolf:s2' ) ; interface PotentialSlotMatch { slot : Slot ; abilityName : string ; entity : string ; } interface MatchNotValidData extends SlotId { value : any ; } const hasSlotBeenFilledThisStage = ( filledArray , slotId ) : boolean => { const { slotName , abilityName } = slotId ; return filledArray . findIndex ( _ => _ . slotName === slotName && _ . abilityName === abilityName ) > - 1 ; } ; param store param convoState param abilities returns export default function fillSlot ( store , convoState , abilities ) { const { dispatch , getState } = store ; dispatch ( startFillSlotStage ( ) ) ; /**
     * keeps track if a slot match has been found for an entity
     * either in focused or all ability
     */ let potentialMatchFoundFlag = false ; /**
     * keep track of all slots that have been filled this stage
     */ let slotFillArr = [ ] ; /**
     * matched slot that fails validation during stage
     */ let matchNotValid = null ; /**
     * save validator fail reason for prompted slot
     */ let promptedSlotReason = null ; const message = getMessageData ( getState ( ) ) ; logState ( getState ( ) ) ; log ( 'first wolf checks to see if there is anything in the runOnFillStack' ) ; const runOnFillStack = getRunOnFillStack ( getState ( ) ) ; if ( runOnFillStack ) { runOnFillStack . forEach ( ( onFillStackItem ) => { const { slotName , abilityName , message } = onFillStackItem ; const actions = fulfillSlot ( convoState , abilities , message , slotName , abilityName , getState ) ; actions . forEach ( dispatch ) ; dispatch ( removeSlotFromOnFillStack ( { slotName , abilityName } ) ) ; } ) ; } if ( isPromptStatus ( getState ( ) ) ) { log ( 'was prompted previous turn' ) ; const promptedSlotInfo = getPromptedSlotId ( getState ( ) ) ; const { slotName , abilityName } = promptedSlotInfo ; log ( '%s on %s was prompted' , slotName , abilityName ) ; const promptedSlot = getSlotBySlotId ( abilities , { slotName , abilityName } ) ; let validatorResult ; if ( promptedSlot ) { if ( message . rawText ) { log ( 'user said: %s' , message . rawText ) ; validatorResult = runSlotValidator ( promptedSlot , message . rawText , message ) ; if ( validatorResult . isValid ) { log ( 'users response was valid according to the prompted slots validator' ) ; const fulfillSlotResult = fulfillSlot ( convoState , abilities , message . rawText , slotName , abilityName , getState ) ; fulfillSlotResult . forEach ( dispatch ) ; log ( 'so fulfill the slot by running these actions: %O' , fulfillSlotResult ) ; slotFillArr . push ( { slotName , abilityName } ) ; dispatch ( removeSlotFromPromptedStack ( { slotName , abilityName } ) ) ; log ( 'exiting stage' ) ; return ; } promptedSlotReason = validatorResult ; matchNotValid = { slotName , abilityName , value : message . rawText } ; } } } if ( ! isFocusedAbilitySet ( getState ( ) ) ) { log ( 'focused ability is not set, so ...' ) ; const msgIntent = message . intent ? message . intent : getDefaultAbility ( getState ( ) ) ; log ( 'setting it to %s' , msgIntent ) ; dispatch ( setFocusedAbility ( msgIntent ) ) ; } const focusedAbility = getFocusedAbility ( getState ( ) ) ; if ( focusedAbility ) { log ( 'there is a focused ability, %s' , focusedAbility ) ; const ability = getTargetAbility ( abilities , focusedAbility ) ; if ( ability && ability . slots . length === 0 ) { log ( 'the focused ability has no slots, marking it completed, and exit stage' ) ; dispatch ( abilityCompleted ( ability . name ) ) ; return ; } log ( 'focused ability has slots, continuing' ) ; } if ( ! isEntityPresent ( message ) ) { log ( 'there is no entities from messageData' ) ; runRetryCheck ( dispatch , getState , convoState , abilities , matchNotValid , potentialMatchFoundFlag , slotFillArr ) ; if ( promptedSlotReason ) { tryPromptedSlotValidatorMessage ( dispatch , getState , promptedSlotReason ) ; } log ( 'finished running "runRetryCheck", exit stage' ) ; return ; } log ( 'there is at least one entity from message Data' ) ; if ( focusedAbility ) { log ( 'starting to look for potential matches in %s which is the focused ability' , focusedAbility ) ; const ability = getTargetAbility ( abilities , focusedAbility ) ; if ( ability ) { log ( '%s ability Found in abilities.ts' , ability . name ) ; const slotMatchesFocusedAbility = getPotentialMatches ( message . entities , ability ) ; slotMatchesFocusedAbility . forEach ( ( match ) => { const checkValandFillResult = checkValidatorAndFill ( store , convoState , abilities , match ) ; if ( checkValandFillResult ) { log ( 'match valid and filled for %s' , checkValandFillResult . slotName ) ; slotFillArr . push ( { slotName : checkValandFillResult . slotName , abilityName : checkValandFillResult . abilityName } ) ; log ( 'exiting stage' ) ; return ; } log ( 'match is not valid for %s' , match . slot . name ) ; matchNotValid = { slotName : match . slot . name , abilityName : match . abilityName , value : match . entity } ; } ) ; if ( slotMatchesFocusedAbility . length > 0 ) { log ( '%s matched in focused ability!' , slotMatchesFocusedAbility . map ( _ => _ . slot . name ) . join ( ', ' ) ) ; potentialMatchFoundFlag = true ; log ( 'checking to see if retry is necessary in retry' ) ; runRetryCheck ( dispatch , getState , convoState , abilities , matchNotValid , potentialMatchFoundFlag , slotFillArr ) ; if ( promptedSlotReason ) { tryPromptedSlotValidatorMessage ( dispatch , getState , promptedSlotReason ) ; } log ( 'checked retry, now exiting' ) ; return ; } } } log ( 'there was no slots matched on focused ability, so checking all abilities for potential matches' ) ; if ( message . intent ) { log ( 'there is intent in the messageData' ) ; const ability = getTargetAbility ( abilities , message . intent ) ; if ( ability && ability . slots . length === 0 ) { log ( 'ability is found, but there is no slots on the ability.' ) ; dispatch ( abilityCompleted ( ability . name ) ) ; if ( promptedSlotReason ) { tryPromptedSlotValidatorMessage ( dispatch , getState , promptedSlotReason ) ; } log ( 'exiting' ) ; return ; } if ( ability ) { log ( 'ability match found and there are slots to check for matches' ) ; const slotMatchesAllAbility = getPotentialMatches ( message . entities , ability ) ; log ( 'slot matched on the ability: %s' , slotMatchesAllAbility . map ( _ => _ . slot . name ) . join ( ', ' ) ) ; log ( 'running the validator on them and filling the slot' ) ; slotMatchesAllAbility . forEach ( ( match ) => { const checkValandFillResult = checkValidatorAndFill ( store , convoState , abilities , match ) ; if ( checkValandFillResult ) { log ( 'match valid and filled for %s' , checkValandFillResult . slotName ) ; slotFillArr . push ( { slotName : checkValandFillResult . slotName , abilityName : checkValandFillResult . abilityName } ) ; log ( 'exiting stage' ) ; return ; } log ( 'match is not valid for %s' , match . slot . name ) ; matchNotValid = { slotName : match . slot . name , abilityName : match . abilityName , value : match . entity } ; } ) ; log ( 'done validating and filling the slots' ) ; if ( slotMatchesAllAbility . length > 0 ) { log ( 'found slot matches, so setting the potentialmatchFoundFlag to true' ) ; potentialMatchFoundFlag = true ; log ( 'running Retry Check' ) ; runRetryCheck ( dispatch , getState , convoState , abilities , matchNotValid , potentialMatchFoundFlag , slotFillArr ) ; if ( promptedSlotReason ) { tryPromptedSlotValidatorMessage ( dispatch , getState , promptedSlotReason ) ; } log ( 'exiting stage' ) ; return ; } } log ( 'ability was not found from the message intent, so the intent on messageData does not exist in abilities.ts' ) ; } log ( 'matches that are valid should have been all filled now' ) ; log ( 'check to see if any not valid slots needs to run retry' ) ; runRetryCheck ( dispatch , getState , convoState , abilities , matchNotValid , potentialMatchFoundFlag , slotFillArr ) ; if ( promptedSlotReason ) { tryPromptedSlotValidatorMessage ( dispatch , getState , promptedSlotReason ) ; } log ( 'exit stage' ) ; return ; } /**
 * Run slot onFill() and return dispatch actions to
 * add message to output queue and store the submittedValue into the pendingData state.
 */ function fulfillSlot ( convoState , abilities , message , slotName , abilityName , getState ) { log ( 'in fulfillSlot()..' ) ; const slot = getSlotBySlotId ( abilities , { slotName , abilityName } ) ; const actions = [ ] ; if ( slot ) { log ( '%O, slot exists:' , slot ) ; const setSlotFuncs = { setSlotEnabled : ( abilityName , slotName , isEnabled ) => { if ( isEnabled ) { actions . push ( disableSlot ( { slotName , abilityName } ) ) ; } else { actions . push ( enableSlot ( { slotName , abilityName } ) ) ; } } , setSlotValue : ( abilityName , slotName , value , runOnFill ? ) => { actions . push ( fillSlotAction ( slotName , abilityName , value ) ) ; if ( runOnFill ) { const targetSlot = findSlotInAbilitiesBySlotId ( abilities , { abilityName , slotName } ) ; if ( ! targetSlot ) { throw new Error ( 'There is no slot with that name' ) ; } const setActions = fulfillSlot ( convoState , abilities , value , slotName , abilityName , getState ) ; actions . push ( ... setActions ) ; } } , setSlotDone : ( abilityName , slotName , isDone ) => { actions . push ( setSlotDone ( { slotName , abilityName } , isDone ) ) ; } , fulfillSlot : ( abilityName , slotName , value ) => { actions . push ( fillSlotAction ( slotName , abilityName , value ) ) ; const targetSlot = findSlotInAbilitiesBySlotId ( abilities , { abilityName , slotName } ) ; if ( ! targetSlot ) { throw new Error ( 'There is no slot with that name' ) ; } const setActions = fulfillSlot ( convoState , abilities , value , slotName , abilityName , getState ) ; actions . push ( ... setActions ) ; } } ; const confirmFuncs = { requireConfirmation : ( targetSlotName ) => { actions . push ( reqConfirmSlot ( { abilityName , slotName } , { abilityName , slotName : targetSlotName } ) ) ; } , accept : ( ) => { const originSlotId = getRequestingSlotIdFromCurrentSlotId ( getState ( ) , { slotName , abilityName } ) ; actions . push ( acceptSlot ( originSlotId ) ) ; } , deny : ( ) => { const originSlotId = getRequestingSlotIdFromCurrentSlotId ( getState ( ) , { slotName , abilityName } ) ; actions . push ( denySlot ( originSlotId ) ) ; actions . push ( addSlotToPromptedStack ( originSlotId , PromptSlotReason . query ) ) ; } } ; actions . push ( fillSlotAction ( slotName , abilityName , message ) ) ; if ( slot . onFill ) { log ( 'slot.onFill exists.. run slot.onFill()' ) ; const fillString = slot . onFill ( message , convoState , setSlotFuncs , confirmFuncs ) ; actions . push ( removeSlotFromOnFillStack ( { slotName , abilityName } ) ) ; if ( fillString ) { log ( 'slot.onFill() return string: %s' , fillString ) ; const message = { message : fillString , type : OutputMessageType . slotFillMessage , slotName : slot . name , abilityName : abilityName } ; actions . push ( addMessage ( message ) ) ; } } const promptedSlotId = getPromptedSlotId ( getState ( ) ) ; if ( promptedSlotId ) { const { slotName : promptedSlotName , abilityName : promptedAbilityName } = promptedSlotId ; if ( slotName === promptedSlotName && abilityName === promptedAbilityName ) { actions . push ( removeSlotFromPromptedStack ( promptedSlotId ) ) ; } } } log ( 'exiting fulfillSlot()..' ) ; return actions ; } /**
 * Check if slot.retry() is required on either prompted slot or newly identified slot.
 * If required, run slot retry and add retry message to output queue
 */ function runRetryCheck ( dispatch , getState , convoState , abilities , matchNotValid , potentialMatchFoundFlag , slotFillArr ) { log ( 'in runRetryCheck().., matchNotValid: %O, potentialMatch: %s, slotFill: %O' , matchNotValid , potentialMatchFoundFlag , slotFillArr ) ; log ( 'checking if there are alternative slot matches that have not been filled..' ) ; if ( matchNotValid === null ) { return ; } const { slotName , abilityName , value } = matchNotValid ; const hasSlotBeenFilled = hasSlotBeenFilledThisStage ( slotFillArr , { slotName , abilityName } ) ; if ( hasSlotBeenFilled ) { log ( 'slot: %s on %s has been filled, so exiting runRetryCheck...' , slotName , abilityName ) ; return ; } if ( potentialMatchFoundFlag ) { log ( 'alternative slot has been found but not filled.. run retry on %s, %s' , slotName , abilityName ) ; dispatch ( incrementTurnCountBySlotId ( { slotName , abilityName } ) ) ; const retryResult = runRetry ( convoState , getState , abilities , slotName , abilityName , value ) ; retryResult . forEach ( dispatch ) ; log ( 'adding %s slot to stack' , slotName ) ; const slotId = { slotName : slotName , abilityName : abilityName } ; dispatch ( addSlotToPromptedStack ( slotId , PromptSlotReason . retry ) ) ; log ( 'exiting runRetryCheck()..' ) ; return ; } log ( 'no alternative slot matches to run retry..' ) ; log ( 'check if there is a previously prompted slot that has not been filled..' ) ; if ( isPromptStatus ( getState ( ) ) ) { const message = getMessageData ( getState ( ) ) ; const promptedSlotInfo = getPromptedSlotId ( getState ( ) ) ; log ( 'run retry on %s' , promptedSlotInfo . slotName ) ; dispatch ( incrementTurnCountBySlotId ( { slotName , abilityName } ) ) ; const retryResult = runRetry ( convoState , getState , abilities , promptedSlotInfo . slotName , promptedSlotInfo . abilityName , message . rawText ) ; retryResult . forEach ( dispatch ) ; log ( 'exiting runRetryCheck()..' ) ; return ; } log ( 'no previously prompted slot to run retry..' ) ; log ( 'exiting runRetryCheck()..' ) ; return ; } /**
 * Run slot retry.
 */ function runRetry ( convoState , getState , abilities , slotName , abilityName , submittedData ) { log ( 'in runRetry()..' ) ; const slot = getSlotBySlotId ( abilities , { slotName , abilityName } ) ; log ( 'getting slot: %s' , slot ) ; if ( slot ) { log ( 'slot exists run slot.retry()' ) ; const turnCount = getSlotTurnCount ( getState ( ) , { slotName , abilityName } ) ; const retryMessage = slot . retry ( convoState , submittedData , turnCount ) ; const message = { message : retryMessage , type : OutputMessageType . retryMessage , slotName : slot . name , abilityName : abilityName } ; log ( 'retry message string: %s' , message . message ) ; log ( 'exiting runRetry()..' ) ; return [ addMessage ( message ) , setSlotPrompted ( { slotName , abilityName } , true ) ] ; } log ( 'exiting runRetry()..' ) ; return [ ] ; } /**
 * If validatorResult has a reason, create an OutputMessageItem and return addMessage() action.
 */ function createValidatorReasonMessage ( validatorResult , slotName , abilityName ) { if ( validatorResult . reason ) { const message = { message : validatorResult . reason , type : OutputMessageType . validateReason , slotName : slotName , abilityName : abilityName } ; return [ addMessage ( message ) ] ; } return [ ] ; } /**
 * For all found matches, run validator and try to fill slot.
 * For all validators that pass, fulfill slot, add message to output queue then exit.
 * For all validators that do not pass, exit.
 */ function checkValidatorAndFill ( store , convoState , abilities , match ) { log ( 'in checkValidatorAndFill()..' ) ; const { dispatch , getState } = store ; const validatorResult = runSlotValidator ( match . slot , match . entity , getMessageData ( getState ( ) ) ) ; log ( 'validatorResult.isValid: %s' , validatorResult . isValid ) ; if ( validatorResult . isValid ) { log ( 'isValid is true.. fulfillSlot' ) ; const fulfillSlotResult = fulfillSlot ( convoState , abilities , match . entity , match . slot . name , match . abilityName , getState ) ; fulfillSlotResult . forEach ( dispatch ) ; log ( 'exiting checkValidatorAndFill with true' ) ; return { slotName : match . slot . name , abilityName : match . abilityName } ; } log ( 'isValid is false.. create validator message' ) ; const validateMessage = createValidatorReasonMessage ( validatorResult , match . slot . name , match . abilityName ) ; validateMessage . forEach ( dispatch ) ; log ( 'exiting checkValidatorAndFill with false' ) ; return null ; } /**
 * Run slot validator.
 */ function runSlotValidator ( slot , submittedData , messageData ) { if ( ! slot . validate ) { return { isValid : true , reason : null } ; } return slot . validate ( submittedData , messageData ) ; } /**
 * Check if entity array contains elements.
 */ function isEntityPresent ( value ) { if ( value . entities . length > 0 ) { return true ; } return false ; } /**
 * For each entity, check if the `entity.name` and `ability` given matches with any slot
 * in the `abilities`
 */ function getPotentialMatches ( entities , targetAbility ) { const slotMatches = targetAbility . slots . filter ( ( slot ) => entities . find ( ( entity ) => entity . name === slot . name ) ) ; const matches = slotMatches . map ( ( slot ) => { const entityMatch = entities . find ( ( entity ) => entity . name === slot . name ) ; if ( entityMatch ) { return { slot , abilityName : targetAbility . name , entity : entityMatch . value } ; } } ) . filter ( _ => _ ) as PotentialSlotMatch [ ] ; if ( matches . length === 0 ) { return [ ] ; } return matches ; } function tryPromptedSlotValidatorMessage ( dispatch , getState , validatorResult ) { const promptedSlotId = getPromptedSlotId ( getState ( ) ) ; if ( ! promptedSlotId ) { return ; } const { slotName , abilityName } = promptedSlotId ; const slotsFilledArr = getfilledSlotsOnCurrentTurn ( getState ( ) ) ; const isOriginalSlotFilled = slotsFilledArr . some ( _ => _ . slotName === slotName && _ . abilityName === abilityName ) ; if ( isOriginalSlotFilled ) { return ; } log ( 'users response was not valid.. creating the validator message' ) ; const validateMessage = createValidatorReasonMessage ( validatorResult , slotName , abilityName ) ; validateMessage . forEach ( dispatch ) ; }