clwoz-models
Version:
Models for ConversationLearner
149 lines (119 loc) • 5.49 kB
text/typescript
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
// TODO: Copied from ConversationLearner-UI. Need to improve Action design to find out how to consolidate knowledge to single layer
// Originally ConversationLearner-models was intended to know have to understand the tree structure which is why we added the
// pre-serialized 'text' field; however, now can't use the text field becuase it includes entity id's instead of human-readable names
// Instead of having to decide wether to perform substitutions from the serialized text or from the tree it's better to
// just have single method used everywhere
// Can't import because it's from ConversationLearner-UI
//import { NodeTypes } from "./models";
enum NodeTypes {
Mention = 'mention-inline-node',
Optional = 'optional-inline-node'
}
// Based on: https://github.com/ianstormtaylor/slate/blob/master/packages/slate-plain-serializer/src/index.js
export interface IOptions {
fallbackToOriginal: boolean
preserveOptionalNodeWrappingCharacters: boolean
}
const defaultOptions: IOptions = {
fallbackToOriginal: false,
preserveOptionalNodeWrappingCharacters: false
}
function serialize(value: any, entityValuesMap: Map<string, string>, userOptions: Partial<IOptions> = {}): string {
const options = {
...defaultOptions,
...userOptions
}
const valueAsJson = typeof value.toJSON === 'function' ? value.toJSON() : value
const processedDocument = removeOptionalNodesWithoutEntityValues(valueAsJson.document, Array.from(entityValuesMap.keys()))
return serializeNode(processedDocument, entityValuesMap, options)
}
/**
* Given node return filter out optional nodes without matching values provided
*
* E.g. You are welcome[, $name] -> You are welcome
* @param node Slate Node
* @param entityValues Key Value pair of entity id to entity display value
*/
function removeOptionalNodesWithoutEntityValues(node: any, entityIds: string[]): any | undefined {
if (node.kind === 'inline' && node.type === NodeTypes.Optional) {
const entityIdsWithinOptionalNode = getEntityIds(node)
const hasValues = entityIdsWithinOptionalNode.every(x => entityIds.includes(x))
return hasValues ? node : undefined
}
if (Array.isArray(node.nodes)) {
node.nodes = node.nodes.map((n: any) => removeOptionalNodesWithoutEntityValues(n, entityIds)).filter((n: any) => n)
}
return node
}
function getEntityIds(node: any): string[] {
const entityIds: string[] = []
// If current node is inline node which we know to have entityId then save it in the list
if (node.kind === 'inline' && node.type === NodeTypes.Mention) {
// This check is required because when input is Slate Value node is Immutable.Map object
// but it could also be a node from value.toJSON()
const data = typeof node.data.toJS === 'function' ? node.data.toJS() : node.data
const option = data.option
if (!option) {
throw new Error(`Attempting to serialize inline node but it did not have option`)
}
const entityId = option.id
entityIds.push(entityId)
}
// Technically this would never get called because inline nodes shouldn't have other children which are inline nodes
// however, it's good to have working depth-first-traversal anyways
if (Array.isArray(node.nodes)) {
const childrenEntityIds: string[] = node.nodes
.map((n: any) => getEntityIds(n))
.reduce((totalIds: string[], nodeIds: string[]) => [...totalIds, ...nodeIds], [])
entityIds.push(...childrenEntityIds)
}
return entityIds
}
function serializeNode(node: any, entityValues: Map<string, string>, options: IOptions): string {
if (node.kind === 'text') {
return node.leaves.map((n: any) => n.text).join('')
}
const serializedChildNodes = node.nodes.map((n: any) => serializeNode(n, entityValues, options))
if (node.kind === 'inline' && node.type === NodeTypes.Mention) {
// This check is required because when input is Slate Value node is Immutable.Map object
// but it could also be a node from value.toJSON()
const data = typeof node.data.toJS === 'function' ? node.data.toJS() : node.data
if (!data.completed) {
return serializedChildNodes.join('')
}
const option = data.option
if (!option) {
throw new Error(`Attempting to serialize inline node but it did not have option`)
}
const entityId = option.id
const mapContainsEntity = entityValues.has(entityId)
if (!mapContainsEntity) {
if (options.fallbackToOriginal) {
return serializedChildNodes.join('')
}
const entityValuesString = Array.from(entityValues.entries())
.map(([id, value]) => `${id}: ${value}`)
.join(', ')
throw new Error(
`Inline node representing entity ${entityId} was NOT provided a value in the given entityValue map: [${entityValuesString}]`
)
}
return entityValues.get(entityId)!
}
if (node.kind === 'document') {
return serializedChildNodes.join('\n')
}
const serializedChildren = serializedChildNodes.join('')
return node.kind === 'inline' && node.type === NodeTypes.Optional
? options.preserveOptionalNodeWrappingCharacters
? serializedChildren
: serializedChildren.slice(1, -1)
: serializedChildren
}
export default {
serialize
}