clwoz-models
Version:
Models for ConversationLearner
486 lines (416 loc) • 16.1 kB
text/typescript
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import EntityIdSerializer, { IOptions } from './slateSerializer'
import { ScoredBase, ScoredAction } from './Score'
import { CallbackResult } from './Callback'
export enum ActionTypes {
TEXT = 'TEXT',
API_LOCAL = 'API_LOCAL',
CARD = 'CARD',
END_SESSION = 'END_SESSION',
SET_ENTITY = 'SET_ENTITY',
DISPATCH = 'DISPATCH',
CHANGE_MODEL = 'CHANGE_MODEL',
}
export enum ConditionType {
EQUAL = "EQUAL",
NOT_EQUAL = "NOT_EQUAL",
GREATER_THAN = "GREATER_THAN",
GREATER_THAN_OR_EQUAL = "GREATER_THAN_OR_EQUAL",
LESS_THAN = "LESS_THAN",
LESS_THAN_OR_EQUAL = "LESS_THAN_OR_EQUAL",
STRING_EQUAL = "STRING_EQUAL",
}
export enum ComparisonType {
NUMBER_OF_VALUES = "NUMBER_OF_VALUES",
NUMERIC_VALUE = "NUMERIC_VALUE",
STRING = "STRING",
}
export interface Condition {
entityId: string
valueId?: string
value?: number
stringValue?: string
condition: ConditionType
}
export interface ActionClientData {
// Used to match import utterances
actionHashes?: string[]
lgName?: string
mockResults: CallbackResult[]
}
// Need dummy actionId for stub action
export const CL_STUB_IMPORT_ACTION_ID = '51cd7df5-e504-451d-b629-0932e604689c'
export class ActionBase {
actionId: string
actionType: ActionTypes
createdDateTime: string
payload: string
isTerminal: boolean
// If true, CL will generate out of domain example utterances that point to this action.
// There should be at most one action in a model with this flag set to `true`.
isEntryNode?: boolean
// Should server train for reprompting and if so what action to take when reprompting
repromptActionId?: string
requiredEntitiesFromPayload: string[]
requiredEntities: string[] = []
negativeEntities: string[] = []
requiredConditions: Condition[] = []
negativeConditions: Condition[] = []
suggestedEntity: string | undefined
version: number
packageCreationId: number
packageDeletionId: number
entityId: string | undefined
enumValueId: string | undefined
clientData?: ActionClientData
constructor(action: ActionBase) {
this.actionId = action.actionId
this.actionType = action.actionType
this.createdDateTime = action.createdDateTime
this.payload = action.payload
this.isTerminal = action.isTerminal
this.isEntryNode = action.isEntryNode
this.repromptActionId = action.repromptActionId
this.requiredEntitiesFromPayload = action.requiredEntitiesFromPayload
this.requiredEntities = action.requiredEntities
this.negativeEntities = action.negativeEntities
this.requiredConditions = action.requiredConditions
this.negativeConditions = action.negativeConditions
this.suggestedEntity = action.suggestedEntity
this.version = action.version
this.packageCreationId = action.packageCreationId
this.packageDeletionId = action.packageDeletionId
this.entityId = action.entityId
this.enumValueId = action.enumValueId
this.clientData = action.clientData
}
// TODO: Refactor away from generic GetPayload for different action types
// They all return strings but the strings are very different (Text is the substituted values, but other actions dont)
// This causes issue of having to pass in entityValueMap even when it's not required, but making it optional ruins
// safety for those places which should require it.
// TODO: Remove ScoredAction since it doesn't have payload
static GetPayload(action: ActionBase | ScoredBase, entityValues: Map<string, string>): string {
if (this.isPVAContent(action)) {
let pvaAction = new PVAAction(action as ActionBase)
return pvaAction.renderValue(entityValues)
}
switch (action.actionType) {
case ActionTypes.TEXT: {
/**
* For backwards compatibility check if payload is of new TextPayload type
* Ideally we would implement schema refactor:
* 1. Make payloads discriminated unions (E.g. After checking the action.type, flow control knows the type of the payload property)
* This removes the need for the GetPayload function and GetArguments which are brittle coding patterns.
*/
try {
const textPayload = JSON.parse(action.payload) as TextPayload
return EntityIdSerializer.serialize(textPayload.json, entityValues)
} catch (e) {
const error = e as Error
throw new Error(
`Error when attempting to parse text action payload. This might be an old action which was saved as a string. Please create a new action. ${
error.message
}`
)
}
}
case ActionTypes.END_SESSION: {
const textPayload = JSON.parse(action.payload) as TextPayload
return EntityIdSerializer.serialize(textPayload.json, entityValues)
}
case ActionTypes.CARD: {
// For API or CARD the payload field of the outer payload is the name of API or the filename of the card template without extension
let cardPayload = JSON.parse(action.payload) as CardPayload
return cardPayload.payload
}
case ActionTypes.API_LOCAL: {
let actionPayload = JSON.parse(action.payload) as ActionPayload
return actionPayload.payload
}
case ActionTypes.DISPATCH: {
// TODO: Another reason to schema refactor...
let actionPayload = JSON.parse(action.payload) as ModelPayload
return `${ActionTypes.DISPATCH}: ${actionPayload.modelName}`
}
case ActionTypes.CHANGE_MODEL: {
const actionPayload = JSON.parse(action.payload) as ModelPayload
return `${ActionTypes.CHANGE_MODEL}: ${actionPayload.modelName}`
}
default:
return action.payload
}
}
// Return true if action is a placeholder
static isPlaceholderAPI(action: Partial<ActionBase> | undefined): boolean {
if (!action) {
return false
}
if (action.payload && JSON.parse(action.payload).isPlaceholder) {
return true
}
return false
}
// Return true if action should be rendered as PVA content
static isPVAContent(action: Partial<ActionBase> | undefined): boolean {
if (!action) {
return false
}
if (action.payload && JSON.parse(action.payload).isPVAContent) {
return true
}
return false
}
// Create dummy placeholder API action
static createPlaceholderAPIAction(placeholderName: string, isTerminal: boolean): ActionBase {
return new ActionBase({
actionId: null!,
payload: JSON.stringify({ payload: placeholderName, logicArguments: [], renderArguments: [], isPlaceholder: true }),
createdDateTime: new Date().toJSON(),
isTerminal,
isEntryNode: false,
requiredEntitiesFromPayload: [],
requiredEntities: [],
negativeEntities: [],
requiredConditions: [],
negativeConditions: [],
suggestedEntity: undefined,
version: 0,
packageCreationId: 0,
packageDeletionId: 0,
actionType: ActionTypes.API_LOCAL,
entityId: undefined,
enumValueId: undefined
})
}
/** Return arguments for an action */
static GetActionArguments(action: ActionBase | ScoredAction): ActionArgument[] {
if (ActionTypes.CARD === action.actionType) {
let cardPayload = JSON.parse(action.payload) as CardPayload
return cardPayload.arguments.map(aa => new ActionArgument(aa))
} else if (action.actionType === ActionTypes.API_LOCAL) {
let actionPayload = JSON.parse(action.payload) as ActionPayload
return [...actionPayload.logicArguments, ...actionPayload.renderArguments].map(aa => new ActionArgument(aa))
}
return []
}
}
export interface ActionList {
actions: ActionBase[]
}
export interface ActionIdList {
actionIds: string[]
}
// TODO: Remove was originally storing two properties text/json
// but now text is removed and this is only here for backwards compatibility
export interface TextPayload {
json: object
}
export interface ActionPayload {
payload: string
logicArguments: IActionArgument[]
renderArguments: IActionArgument[]
isPlaceholder?: boolean
// TODO: Remove after consolidation with placeholder
// Has different behavior than placeholders and implies only mock results can be used
isCallbackUnassigned?: boolean
}
export interface ActionPayloadSingleArguments {
payload: string
arguments: IActionArgument[]
}
export interface CardPayload {
payload: string
arguments: IActionArgument[]
}
export interface IActionArgument {
parameter: string
value: TextPayload
}
export class ActionArgument {
parameter: string
value: object
constructor(actionArgument: IActionArgument) {
this.parameter = actionArgument.parameter
this.value = actionArgument.value.json
}
renderValue(entityValues: Map<string, string>, serializerOptions: Partial<IOptions> = {}): string {
return EntityIdSerializer.serialize(this.value, entityValues, serializerOptions)
}
}
export interface RenderedActionArgument {
parameter: string
value: string | null
}
export class TextAction extends ActionBase {
value: object // json slate value
constructor(action: ActionBase) {
super(action)
if (action.actionType !== ActionTypes.TEXT) {
throw new Error(`You attempted to create text action from action of type: ${action.actionType}`)
}
this.value = JSON.parse(this.payload).json
}
renderValue(entityValues: Map<string, string>, serializerOptions: Partial<IOptions> = {}): string {
return EntityIdSerializer.serialize(this.value, entityValues, serializerOptions)
}
}
export class ApiAction extends ActionBase {
name: string
logicArguments: ActionArgument[]
renderArguments: ActionArgument[]
isPlaceholder?: boolean
isCallbackUnassigned?: boolean
constructor(action: ActionBase) {
super(action)
if (action.actionType !== ActionTypes.API_LOCAL) {
throw new Error(`You attempted to create api action from action of type: ${action.actionType}`)
}
const actionPayload: ActionPayload = JSON.parse(this.payload)
this.name = actionPayload.payload
this.logicArguments = actionPayload.logicArguments ? actionPayload.logicArguments.map(aa => new ActionArgument(aa)) : []
this.renderArguments = actionPayload.renderArguments ? actionPayload.renderArguments.map(aa => new ActionArgument(aa)) : []
this.isPlaceholder = actionPayload.isPlaceholder
this.isCallbackUnassigned = actionPayload.isCallbackUnassigned
}
renderLogicArguments(entityValues: Map<string, string>, serializerOptions: Partial<IOptions> = {}): RenderedActionArgument[] {
return this.renderArgs(this.logicArguments, entityValues, serializerOptions)
}
renderRenderArguments(entityValues: Map<string, string>, serializerOptions: Partial<IOptions> = {}): RenderedActionArgument[] {
return this.renderArgs(this.renderArguments, entityValues, serializerOptions)
}
private renderArgs(
args: ActionArgument[],
entityValues: Map<string, string>,
serializerOptions: Partial<IOptions> = {}
): RenderedActionArgument[] {
return args.map(aa => {
let value = null
try {
value = EntityIdSerializer.serialize(aa.value, entityValues, serializerOptions)
} catch (error) {
// Just return null if argument doesn't have a value
}
return {
...aa,
value: value
}
})
}
}
export class CardAction extends ActionBase {
templateName: string
arguments: ActionArgument[]
constructor(action: ActionBase) {
super(action)
if (action.actionType !== ActionTypes.CARD) {
throw new Error(`You attempted to create card action from action of type: ${action.actionType}`)
}
const payload: CardPayload = JSON.parse(this.payload)
this.templateName = payload.payload
this.arguments = payload.arguments.map(aa => new ActionArgument(aa))
}
renderArguments(entityValues: Map<string, string>, serializerOptions: Partial<IOptions> = {}): RenderedActionArgument[] {
return this.arguments.map(aa => {
let value = null
try {
value = EntityIdSerializer.serialize(aa.value, entityValues, serializerOptions)
} catch (error) {
// Just return null if argument doesn't have a value
}
return {
...aa,
value: value
}
})
}
}
export class SessionAction extends ActionBase {
value: object // json slate value
constructor(action: ActionBase) {
super(action)
if (action.actionType !== ActionTypes.END_SESSION) {
throw new Error(`You attempted to create session action from action of type: ${action.actionType}`)
}
this.value = JSON.parse(this.payload).json
}
renderValue(entityValues: Map<string, string>, serializerOptions: Partial<IOptions> = {}): string {
return EntityIdSerializer.serialize(this.value, entityValues, serializerOptions)
}
}
export class PVAAction extends ActionBase {
value: string
constructor(action: ActionBase) {
super(action)
const payload = JSON.parse(this.payload)
if (!payload.isPVAContent) {
throw new Error(`You attempted to create PVA action for non-PVA content`)
}
this.value = payload.value
}
renderValue(entityValues: Map<string, string>): string {
let output = this.value
entityValues.forEach((value: string, key: string) =>
{
output = output.replace(`{${key}}`, value)
})
return output
}
}
export type SetEntityPayload = {
entityId: string
enumValueId: string
}
export class SetEntityAction extends ActionBase {
entityId: string
enumValueId: string
constructor(action: ActionBase) {
super(action)
if (action.actionType !== ActionTypes.SET_ENTITY) {
throw new Error(`You attempted to create set entity action from action of type: ${action.actionType}`)
}
// TODO: Server already has actual entityId and enumValueId values, should not need to use payload like this
// but some things like scored action only have the payload
const jsonPayload = JSON.parse(this.payload) as SetEntityPayload
this.entityId = jsonPayload.entityId
this.enumValueId = jsonPayload.enumValueId
}
}
export type ModelPayload = {
modelId: string
modelName: string
}
export class ModelAction extends ActionBase {
modelId: string
modelName: string
constructor(action: ActionBase) {
super(action)
if (action.actionType !== ActionTypes.DISPATCH
&& action.actionType !== ActionTypes.CHANGE_MODEL) {
throw new Error(`You attempted to create Model action from action of type: ${action.actionType}`)
}
// TODO: Server already has actual modelId and modelName values, should not need to use payload like this
// but some things like scored action only have the payload
const jsonPayload = JSON.parse(this.payload) as ModelPayload
this.modelId = jsonPayload.modelId
this.modelName = jsonPayload.modelName
}
}
export class DispatchAction extends ModelAction {
constructor(action: ActionBase) {
if (action.actionType !== ActionTypes.DISPATCH) {
throw new Error(`You attempted to create Dispatch action from action of type: ${action.actionType}`)
}
super(action)
}
}
export class ChangeModelAction extends ModelAction {
constructor(action: ActionBase) {
if (action.actionType !== ActionTypes.CHANGE_MODEL) {
throw new Error(`You attempted to create ChangeModel action from action of type: ${action.actionType}`)
}
super(action)
}
}