clwoz-models
Version:
Models for ConversationLearner
435 lines (394 loc) • 15.9 kB
text/typescript
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { ExtractResponse } from './Extract'
import { Teach, TeachResponse } from './Teach'
import { TrainRound, TrainDialog, TrainScorerStep, TextVariation, CreateTeachParams, ExtractorStepType, TrainExtractorStep, OUT_OF_DOMAIN_INPUT } from './TrainDialog'
import { LogDialog, LogRound, LogScorerStep } from './LogDialog'
import { EntityBase, LabeledEntity, PredictedEntity } from './Entity'
import { ActionBase } from './Action'
import { MemoryValue } from './Memory'
import { FilledEntityMap, FilledEntity } from './FilledEntity'
import { AppDefinition } from './AppDefinition'
export class ModelUtils {
public static generateGUID(): string {
let d = new Date().getTime()
let guid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, char => {
let r = ((d + Math.random() * 16) % 16) | 0
d = Math.floor(d / 16)
return (char === 'x' ? r : (r & 0x3) | 0x8).toString(16)
})
return guid
}
/** Remove n words from start of string */
public static RemoveWords(text: string, numWords: number): string {
if (text.length === 0 || numWords === 0) {
return text
}
const firstSpace = text.indexOf(' ')
const remaining = firstSpace > 0 ? text.slice(firstSpace + 1) : ''
numWords--
return this.RemoveWords(remaining, numWords)
}
//====================================================================
// CONVERSION: LabeledEntity == PredictedEntity
//====================================================================
public static ToLabeledEntity(predictedEntity: PredictedEntity): LabeledEntity {
const { score, ...labeledEntity } = predictedEntity
return labeledEntity
}
public static ToLabeledEntities(predictedEntities: PredictedEntity[]): LabeledEntity[] {
let labeledEntities: LabeledEntity[] = []
for (let predictedEntity of predictedEntities) {
let labelEntity = ModelUtils.ToLabeledEntity(predictedEntity)
labeledEntities.push(labelEntity)
}
return labeledEntities
}
public static ToPredictedEntity(labeledEntity: LabeledEntity): PredictedEntity {
return {
...labeledEntity,
score: undefined
}
}
public static ToPredictedEntities(labeledEntities: LabeledEntity[]): PredictedEntity[] {
let predictedEntities: PredictedEntity[] = []
for (let labeledEntity of labeledEntities) {
let predictedEntity = ModelUtils.ToPredictedEntity(labeledEntity)
predictedEntities.push(predictedEntity)
}
return predictedEntities
}
//====================================================================
// CONVERSION: ExtractResponse == TextVariation
//====================================================================
public static ToTextVariation(extractResponse: ExtractResponse): TextVariation {
let labeledEntities = this.ToLabeledEntities(extractResponse.predictedEntities)
let textVariation = {
text: extractResponse.text,
labelEntities: labeledEntities
}
return textVariation
}
public static ToExtractResponse(textVariation: TextVariation): ExtractResponse {
let predictedEntities = this.ToPredictedEntities(textVariation.labelEntities)
let extractResponse: ExtractResponse = {
definitions: {
entities: [],
actions: [],
trainDialogs: []
},
packageId: '',
metrics: {
wallTime: 0
},
text: textVariation.text,
predictedEntities: predictedEntities
}
return extractResponse
}
public static ToExtractResponses(textVariations: TextVariation[]): ExtractResponse[] {
let extractResponses: ExtractResponse[] = []
for (let textVariation of textVariations) {
let predictedEntities = this.ToPredictedEntities(textVariation.labelEntities)
let extractResponse: ExtractResponse = {
definitions: {
entities: [],
actions: [],
trainDialogs: []
},
packageId: '',
metrics: {
wallTime: 0
},
text: textVariation.text,
predictedEntities: predictedEntities
}
extractResponses.push(extractResponse)
}
return extractResponses
}
public static ToTextVariations(extractResponses: ExtractResponse[]): TextVariation[] {
let textVariations: TextVariation[] = []
for (let extractResponse of extractResponses) {
let labelEntities = this.ToLabeledEntities(extractResponse.predictedEntities)
let textVariation: TextVariation = {
text: extractResponse.text,
labelEntities: labelEntities
}
textVariations.push(textVariation)
}
return textVariations
}
//====================================================================
// CONVERSION: LogDialog == TrainDialog
//====================================================================
public static ToTrainDialog(
logDialog: LogDialog,
actions: ActionBase[] | null = null,
entities: EntityBase[] | null = null
): TrainDialog {
let trainRounds: TrainRound[] = []
for (let logRound of logDialog.rounds) {
let trainRound = ModelUtils.ToTrainRound(logRound)
trainRounds.push(trainRound)
}
let appDefinition: AppDefinition | null = null
if (entities !== null && actions !== null) {
appDefinition = {
entities,
actions,
trainDialogs: []
}
}
// Update initialFilledEntity list to include those that were saved in onEndSession
let initialFilledEntities: FilledEntity[] = []
if (trainRounds.length !== 0 && trainRounds[0].scorerSteps.length !== 0) {
// Get entities extracted on first input
const firstEntityIds = trainRounds[0].extractorStep.textVariations[0]
? trainRounds[0].extractorStep.textVariations[0].labelEntities.map(le => le.entityId)
: []
// Intial entities are ones on first round that weren't extracted on the first utterance
initialFilledEntities = trainRounds[0].scorerSteps[0].input.filledEntities
.filter(fe => !firstEntityIds.includes(fe.entityId!))
}
return {
createdDateTime: logDialog.createdDateTime,
lastModifiedDateTime: logDialog.lastModifiedDateTime,
packageCreationId: 0,
packageDeletionId: 0,
trainDialogId: '',
sourceLogDialogId: logDialog.logDialogId,
version: 0,
rounds: trainRounds,
definitions: appDefinition,
initialFilledEntities: initialFilledEntities,
tags: [],
description: ''
}
}
//====================================================================
// CONVERSION: LogRound == TrainRound
//====================================================================
public static ToTrainRound(logRound: LogRound): TrainRound {
return {
extractorStep: {
textVariations: [
{
labelEntities: ModelUtils.ToLabeledEntities(logRound.extractorStep.predictedEntities),
text: logRound.extractorStep.text
}
],
type: ExtractorStepType.USER_INPUT
},
scorerSteps: logRound.scorerSteps.map<TrainScorerStep>(logScorerStep => ({
input: logScorerStep.input,
labelAction: logScorerStep.predictedAction,
logicResult: logScorerStep.logicResult,
scoredAction: undefined,
uiScoreResponse: logScorerStep.predictionDetails
}))
}
}
//====================================================================
// CONVERSION: LogScorerStep == TrainScorerStep
//====================================================================
public static ToTrainScorerStep(logScorerStep: LogScorerStep): TrainScorerStep {
return {
input: logScorerStep.input,
labelAction: logScorerStep.predictedAction,
logicResult: logScorerStep.logicResult,
scoredAction: undefined
}
}
//====================================================================
// CONVERSION: TrainDialog == CreateTeachParams
//====================================================================
public static ToCreateTeachParams(trainDialog: TrainDialog): CreateTeachParams {
let createTeachParams: CreateTeachParams = {
contextDialog: trainDialog.rounds,
sourceLogDialogId: trainDialog.sourceLogDialogId,
initialFilledEntities: trainDialog.initialFilledEntities
}
// TODO: Change to non destructive operation
// Strip out "entityType" (*sigh*)
for (let round of createTeachParams.contextDialog) {
for (let textVariation of round.extractorStep.textVariations) {
for (let labeledEntity of textVariation.labelEntities) {
delete (labeledEntity as any).entityType
}
}
}
return createTeachParams
}
//====================================================================
// CONVERSION: TeachResponse == Teach
//====================================================================
public static ToTeach(teachResponse: TeachResponse): Teach {
return {
teachId: teachResponse.teachId,
trainDialogId: teachResponse.trainDialogId,
createdDatetime: undefined,
lastQueryDatetime: undefined,
packageId: undefined
}
}
//====================================================================
// Misc utils shared between SDK and UI
//====================================================================
public static areEqualTextVariations(tv1: TextVariation, tv2: TextVariation) {
if (tv1.text !== tv2.text) {
return false
}
if (tv1.labelEntities.length !== tv2.labelEntities.length) {
return false
}
for (const le1 of tv1.labelEntities) {
const le2 = tv2.labelEntities.find(
le => le.entityId === le1.entityId && le.entityText === le1.entityText && le.startCharIndex === le1.startCharIndex
)
if (!le2) {
return false
}
}
return true
}
public static areEqualMemoryValues(mvs1: MemoryValue[], mvs2: MemoryValue[]) {
if (mvs1.length !== mvs2.length) {
return false
}
for (let mv1 of mvs1) {
const match = mvs2.find(mv2 => {
if (mv1.userText !== mv2.userText) {
return false
}
if (mv1.displayText !== mv2.displayText) {
return false
}
if (mv1.builtinType !== mv2.builtinType) {
return false
}
if (JSON.stringify(mv1.resolution) !== JSON.stringify(mv2.resolution)) {
return false
}
return true
})
if (!match) {
return false
}
}
return true
}
public static changedFilledEntities(originalEntityMap: FilledEntityMap, newEntityMap: FilledEntityMap): FilledEntity[] {
let changedFilledEntities: FilledEntity[] = []
// Capture emptied entities
for (let entityName in originalEntityMap.map) {
if (!newEntityMap.map[entityName]) {
const filledEntity = {
entityId: originalEntityMap.map[entityName].entityId,
values: []
}
changedFilledEntities.push(filledEntity)
}
}
for (let entityName in newEntityMap.map) {
// Capture new entities
if (!originalEntityMap.map[entityName]) {
changedFilledEntities.push(newEntityMap.map[entityName])
}
// Capture changed entities
else if (!ModelUtils.areEqualMemoryValues(newEntityMap.map[entityName].values, originalEntityMap.map[entityName].values)) {
changedFilledEntities.push(newEntityMap.map[entityName])
}
}
return changedFilledEntities
}
public static userText(extractorStep: TrainExtractorStep, excludedEntities: string[] = [], useMarkdown: boolean = false) {
if (extractorStep.type === ExtractorStepType.OUT_OF_DOMAIN) {
return OUT_OF_DOMAIN_INPUT
}
if (useMarkdown) {
return ModelUtils.textVariationToMarkdown(extractorStep.textVariations[0], excludedEntities)
}
return extractorStep.textVariations[0].text
}
public static textVariationToMarkdown(textVariation: TextVariation, excludeEntities: string[]) {
if (textVariation.labelEntities.length === 0) {
return textVariation.text
}
// Remove resolvers that aren't labelled
let labelEntities = textVariation.labelEntities.filter(le => !excludeEntities.includes(le.entityId))
// Remove duplicate labels
labelEntities = labelEntities.filter((le, i) => labelEntities.findIndex(fi => fi.startCharIndex === le.startCharIndex) === i)
// Remove overlapping labels (can happen if have CUSTOM and Pre-Trained)
labelEntities = labelEntities.filter(le => labelEntities.findIndex(fe => fe.entityId !== le.entityId &&
(le.startCharIndex >= fe.startCharIndex && le.endCharIndex <= fe.endCharIndex)) === -1)
if (labelEntities.length === 0) {
return textVariation.text
}
labelEntities = labelEntities.sort(
(a, b) => (a.startCharIndex > b.startCharIndex ? 1 : a.startCharIndex < b.startCharIndex ? -1 : 0)
)
let text = textVariation.text.substring(0, labelEntities[0].startCharIndex)
for (let index in labelEntities) {
let curEntity = labelEntities[index]
text = `${text}**_${textVariation.text.substring(curEntity.startCharIndex, curEntity.endCharIndex + 1)}_**`
let nextEntity = labelEntities[Number(index) + 1]
if (nextEntity) {
text = `${text}${textVariation.text.substring(curEntity.endCharIndex + 1, nextEntity.startCharIndex)}`
} else {
text = `${text}${textVariation.text.substring(curEntity.endCharIndex + 1, textVariation.text.length)}`
}
}
return text
}
public static PrebuiltDisplayText(builtinType: string, resolution: any, entityText: string): string {
if (!builtinType || !resolution) {
return entityText
}
if (['builtin.geography', 'builtin.encyclopedia'].some(prefix => builtinType.startsWith(prefix))) {
return entityText
}
switch (builtinType) {
case 'builtin.datetimeV2.date':
let date = resolution.values[0].value
if (resolution.values[1]) {
date += ` or ${resolution.values[1].value}`
}
return date
case 'builtin.datetimeV2.time':
let time = resolution.values[0].value
if (resolution.values[1]) {
time += ` or ${resolution.values[1].value}`
}
return time
case 'builtin.datetimeV2.daterange':
return `${resolution.values[0].start} to ${resolution.values[0].end}`
case 'builtin.datetimeV2.timerange':
return `${resolution.values[0].start} to ${resolution.values[0].end}`
case 'builtin.datetimeV2.datetimerange':
return `${resolution.values[0].start} to ${resolution.values[0].end}`
case 'builtin.datetimeV2.duration':
return `${resolution.values[0].value} seconds`
case 'builtin.datetimeV2.set':
return `${resolution.values[0].value}`
case 'builtin.number':
return resolution.value
case 'builtin.ordinal':
return resolution.value
case 'builtin.temperature':
return resolution.value
case 'builtin.dimension':
return resolution.value
case 'builtin.money':
return resolution.value
case 'builtin.age':
return resolution.value
case 'builtin.percentage':
return resolution.value
default:
return entityText
}
}
}