import * as CLM from '@conversationlearner/models' ; import { SourceAndModelPair } from "../types/models" ; import * as uuid from 'uuid/v4' ; import * as Util from './util' ; import { DispatcherAlgorithmType } from '../components/modals/DispatcherCreator' ; /**
 * Is currently the same implementation as third algorithm, but could be different
 * Test data should be different that the data it's testing and also consistent across algorithms
 */ const generateTestData = generateRandomMultiTransferDispatcherSource ; param sourceModelPairs param algorithmType export function generateDispatcherSource ( sourceModelPairs , algorithmType ) { switch ( algorithmType ) { case DispatcherAlgorithmType . DeterministicSingleTransfer : return generaterministicDispatcherSource ( sourceModelPairs , 3 ) ; case DispatcherAlgorithmType . RandomSingleTransfer : return generateRandomTransitonDispatcherSource ( sourceModelPairs , defaultGetPercentageOfRoundsToTransferAt ) ; case DispatcherAlgorithmType . RandomMultiTransfer : return generateRandomMultiTransferDispatcherSource ( sourceModelPairs , 20 , 10 ) ; case DispatcherAlgorithmType . TestData : return generateTestData ( sourceModelPairs , 20 , 10 ) ; default : throw new Error ( `Unhandled algorithm type ${ algorithmType }` ) ; } throw new Error ( `Could not associate Dispatcher Algorithm Type with algorithm. You passed: ${ algorithmType }` ) ; } function defaultGetPercentageOfRoundsToTransferAt ( numOfRounds ) { if ( numOfRounds <= 3 ) { return numOfRounds ; } else if ( numOfRounds <= 10 ) { return Math . round ( numOfRounds * 0.4 ) ; } else if ( numOfRounds <= 20 ) { return Math . round ( numOfRounds * 0.2 ) ; } else { return Math . round ( numOfRounds * 0.15 ) ; } } param transitionAtFirstNRounds function generaterministicDispatcherSource ( sourceModelPairs , transitionAtFirstNRounds ) { generateDispatchActions ( sourceModelPairs ) ; const modelsTrainDialogs = associateModelDialogsWithDispatchActionsAndClean ( sourceModelPairs ) ; const trainDialogs = determisticSingleTransfer ( modelsTrainDialogs , transitionAtFirstNRounds ) ; return { trainDialogs , actions : sourceModelPairs . map ( sm => sm . action ) , entities : [ ] , } ; } param getPercentageOfRoundsToTransferAt function generateRandomTransitonDispatcherSource ( sourceModelPairs , getPercentageOfRoundsToTransferAt ) { generateDispatchActions ( sourceModelPairs ) ; const modelsTrainDialogs = associateModelDialogsWithDispatchActionsAndClean ( sourceModelPairs ) ; const trainDialogs = randomSingleTransfer ( modelsTrainDialogs , getPercentageOfRoundsToTransferAt ) ; return { trainDialogs , actions : sourceModelPairs . map ( sm => sm . action ) , entities : [ ] , } ; } function generateRandomMultiTransferDispatcherSource ( sourceModelPairs , numberOfTransitionsPerDialog , numberOfDialogs ) { generateDispatchActions ( sourceModelPairs ) ; const modelsTrainDialogs = associateModelDialogsWithDispatchActionsAndClean ( sourceModelPairs ) ; const trainDialogs = randomMultiTransfer ( modelsTrainDialogs , numberOfTransitionsPerDialog , numberOfDialogs ) ; return { trainDialogs , actions : sourceModelPairs . map ( sm => sm . action ) , entities : [ ] , } ; } /**
 * Generate 1 Dispatch Action per model and associate with source + model pair
 *
 * Store:
 * - modelId for dispatching
 * - modelName for display
 */ function generateDispatchActions ( sourceModelPairs ) { sourceModelPairs . forEach ( smp => { if ( smp . action ) { return ; } const modelPayload = { modelId : smp . model . appId , modelName : smp . model . appName } ; const dispatchAction = { actionId : uuid ( ) , createdDateTime : new Date ( ) . toJSON ( ) , actionType : CLM . ActionTypes . DISPATCH , payload : JSON . stringify ( modelPayload ) , isTerminal : true , requiredEntitiesFromPayload : [ ] , requiredEntities : [ ] , negativeEntities : [ ] , requiredConditions : [ ] , negativeConditions : [ ] , clientData : { actionHashes : [ ] , } } ; smp . action = dispatchAction ; } ) ; } /**
 * For each dialog in model A set each rounds to use that models Dispatch Action
 * This means, when this input (extraction) is seen, dispatch to this model.
 *
 * Clear all entities, and ensure single scorer step
 */ function associateModelDialogsWithDispatchActionsAndClean ( sourceModelPairs ) { return sourceModelPairs . map ( ( sm , mIndex ) => { if ( ! sm . action ) { throw new Error ( `(Source + Model) pair must have dispatch action assigned at this point` ) ; } return sm . source . trainDialogs . map ( ( t , tIndex ) => { t . rounds . forEach ( r => { r . extractorStep . textVariations . forEach ( tv => { tv . labelEntities = [ ] ; } ) ; const scorerStep = { input : { filledEntities : [ ] , context : { } , maskedActions : [ ] , } , labelAction : sm . action . actionId , logicResult : undefined , scoredAction : undefined , } ; r . scorerSteps = [ scorerStep ] ; } ) ; t . tags = [ `model-${ mIndex + 1 }` , `dialog-${ tIndex + 1 }` ] ; return t ; } ) ; } ) ; } const generateDispatchDialog = ( rounds ) => ( { tags : [ `generated` ] , description : "" , trainDialogId : uuid ( ) , rounds , clientData : { importHashes : [ ] } , initialFilledEntities : [ ] , createdDateTime : new Date ( ) . toJSON ( ) , lastModifiedDateTime : new Date ( ) . toJSON ( ) , } as unknown as CLM . TrainDialog ) ; /**
 * Intermix rounds from different dialogs to implicitly demonstrate dispatching/context switching to other model
 *
 * Example
 * Dialogs:
 *  ModelA:
 *   [A,B,C]
 *  ModelB:
 *   [D,E,F]
 *  ModelC:
 *   [G,H,I]
 * ...
 *
 * Output:
 * [
 *  [A,D,E,F],
 *  [A,B,D,E,F],
 *  [A,B,C,D,E,F],
 *  [A,G,H,I],
 *  [A,B,G,H,I],
 *  [A,B,C,G,H,I],
 *  [D,A,B,C],
 *  [D,E,A,B,C],
 *  [D,E,F,A,B,C],
 *  [D,G,H,I],
 *  [D,E,G,H,I],
 *  [D,E,F,G,H,I],
 *  ...
 * ]
 *
 * Currently only tests single transition.
 * A to B or A to C
 * Could try re-entry patterns: A B A
 * Could try multi model switch: A B C
 */ function determisticSingleTransfer ( modelsTrainDialogs , transitionAtFirstNRounds ) { const allTrainDialogs = modelsTrainDialogs . reduce ( ( a , b ) => [ ... a , ... b ] ) ; const dialogsWithoutModelTransition = Util . deepCopy ( allTrainDialogs ) ; const dialogTransitionGroups = generateDeterministicDialogTransitionGroups ( modelsTrainDialogs , transitionAtFirstNRounds ) ; const dialogsWithModelTransition = concatTransitionDialogsWithOtherDialogs ( dialogTransitionGroups ) . map ( t => generateDispatchDialog ( t . rounds ) ) ; return [ ... dialogsWithoutModelTransition , ... dialogsWithModelTransition ] ; } /**
 * Similar to deterministic transfer except try to transition and random N positions within dialog
 *
 * Array of rounds (with random transition points)
 *      1   2          3    4
 * [1,2,3,4,5,6,7,8,9,10,11,12,13,...]
 *  1   2         3            4
 * [1,2,3,4,5,6,7,8,9,10,11,12,13,...]
 */ function randomSingleTransfer ( modelsTrainDialogs , getPercentageOfRoundsToTransferAt ) { const allTrainDialogs = modelsTrainDialogs . reduce ( ( a , b ) => [ ... a , ... b ] ) ; const dialogsWithoutModelTransition = Util . deepCopy ( allTrainDialogs ) ; const dialogTransitionGroups = generateRandomDialogTransitionGroups ( modelsTrainDialogs , getPercentageOfRoundsToTransferAt ) ; const dialogsWithModelTransition = concatTransitionDialogsWithOtherDialogs ( dialogTransitionGroups ) . map ( t => generateDispatchDialog ( t . rounds ) ) ; return [ ... dialogsWithoutModelTransition , ... dialogsWithModelTransition ] ; } param modelsTrainDialogs param numberOfTransitionsPerDialog param numberOfDialogs function randomMultiTransfer ( modelsTrainDialogs , numberOfTransitionsPerDialog , numberOfDialogs ) { const trainDialogs = [ ] ; const maxRounds = 100 ; const numberOfRoundsPerTransition = Math . floor ( maxRounds / numberOfTransitionsPerDialog ) ; const numberOfRoundsPerDialog = maxRounds - numberOfRoundsPerTransition ; while ( trainDialogs . length < numberOfDialogs ) { let rounds = [ ] ; let previousModelIndex = 0 ; while ( rounds . length < numberOfRoundsPerDialog ) { let modelIndex ; do { modelIndex = Math . floor ( Math . random ( ) * modelsTrainDialogs . length ) ; } while ( modelIndex === previousModelIndex ) ; const dialogsInModel = modelsTrainDialogs [ modelIndex ] ; const dialogIndex = Math . floor ( Math . random ( ) * dialogsInModel . length ) ; const dialog = dialogsInModel [ dialogIndex ] ; const maxIndex = Math . max ( 1 , dialog . rounds . length - numberOfRoundsPerTransition ) ; const startRoundIndex = Math . floor ( Math . random ( ) * maxIndex ) ; let newRounds = dialog . rounds . slice ( startRoundIndex , numberOfRoundsPerTransition ) ; while ( newRounds . length < numberOfRoundsPerTransition ) { newRounds . push ( ... newRounds ) ; } newRounds = newRounds . slice ( 0 , numberOfRoundsPerTransition ) ; rounds . push ( ... newRounds ) ; previousModelIndex = modelIndex ; } const trainDialog = generateDispatchDialog ( rounds ) ; trainDialogs . push ( trainDialog ) ; rounds = [ ] ; } return trainDialogs ; } type DialogTransitionGroup = { trainDialogsToTransitionFrom : CLM . TrainDialog [ ] ; trainDialogsFromOtherModels : CLM . TrainDialog [ ] ; } ; /**
 * For each dialog to transition from, generate dialog for each of the other model dialogs with concatenation of rounds from A to B
 */ function concatTransitionDialogsWithOtherDialogs ( dialogTransitionGroups ) { return dialogTransitionGroups . map ( dialogTransitionGroup => { return dialogTransitionGroup . trainDialogsToTransitionFrom . map ( t => { return dialogTransitionGroup . trainDialogsFromOtherModels . map ( dialogFromOtherModel => { const dialogCopy = Util . deepCopy ( t ) ; dialogCopy . rounds = [ ... dialogCopy . rounds , ... dialogFromOtherModel . rounds , ] ; return dialogCopy ; } ) ; } ) . reduce ( ( a , b ) => [ ... a , ... b ] ) ; } ) . reduce ( ( a , b ) => [ ... a , ... b ] ) ; } function generateRandomDialogTransitionGroups ( modelsTrainDialogs , getPercentageOfRoundsToTransferAt ) { return modelsTrainDialogs . map ( ( modelDialogs , mIndex ) => { /**
         * Original implementation attempted transitions within same model and preserved action
         * This could be good or introduce noise. Consider it as option
         */ const trainDialogsFromOtherModels = modelsTrainDialogs . filter ( ( _ , k ) => k !== mIndex ) . reduce ( ( a , b ) => [ ... a , ... b ] ) ; const trainDialogsToTransitionFrom = modelDialogs . map ( t => { const numRandomTransfersPoints = getPercentageOfRoundsToTransferAt ( t . rounds . length ) ; const transitionRoundIncies = getUniqueRandomNumbers ( numRandomTransfersPoints , t . rounds . length ) ; return transitionRoundIncies . map ( transitionRoundIndex => { const dialogCopy = Util . deepCopy ( t ) ; dialogCopy . rounds = dialogCopy . rounds . slice ( 0 , transitionRoundIndex ) ; return dialogCopy ; } ) ; } ) . reduce ( ( a , b ) => [ ... a , ... b ] ) ; return { trainDialogsToTransitionFrom , trainDialogsFromOtherModels } ; } ) ; } function getUniqueRandomNumbers ( length , max ) { const numbers = [ ] ; while ( numbers . length < length ) { const n = Math . ceil ( Math . random ( ) * max ) ; if ( ! numbers . includes ( n ) ) { numbers . push ( n ) ; } } return numbers ; } function generateDeterministicDialogTransitionGroups ( modelsTrainDialogs , transitionAtFirstNRounds ) { return modelsTrainDialogs . map ( ( modelDialogs , mIndex ) => { /**
         * Original implementation attempted transitions within same model and preserved action
         * This could be good or introduce noise. Consider it as option
         */ const trainDialogsFromOtherModels = modelsTrainDialogs . filter ( ( _ , k ) => k !== mIndex ) . reduce ( ( a , b ) => [ ... a , ... b ] ) ; const trainDialogsToTransitionFrom = modelDialogs . map ( t => { const possibleTransitionRounds = t . rounds . slice ( 0 , transitionAtFirstNRounds ) ; return possibleTransitionRounds . map ( ( _ , rIndex , rounds ) => { const dialogCopy = Util . deepCopy ( t ) ; const roundsUpToTransitionPoint = rounds . slice ( 0 , rIndex + 1 ) ; dialogCopy . rounds = roundsUpToTransitionPoint ; return dialogCopy ; } ) ; } ) . reduce ( ( a , b ) => [ ... a , ... b ] ) ; return { trainDialogsToTransitionFrom , trainDialogsFromOtherModels , } ; } ) ; }