From 0b218adb45caaf251ccc15e2a870ff825300d38a Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen <ohltyler@amazon.com> Date: Wed, 12 Mar 2025 17:00:19 -0700 Subject: [PATCH] Simplify model i/o when interface defined Signed-off-by: Tyler Ohlsen <ohltyler@amazon.com> --- .../input_fields/model_field.tsx | 66 +++--- .../ml_processor_inputs/model_inputs.tsx | 197 ++++++++---------- .../ml_processor_inputs/model_outputs.tsx | 143 +++++-------- 3 files changed, 168 insertions(+), 238 deletions(-) diff --git a/public/pages/workflow_detail/workflow_inputs/input_fields/model_field.tsx b/public/pages/workflow_detail/workflow_inputs/input_fields/model_field.tsx index fc1be031..b8377dbf 100644 --- a/public/pages/workflow_detail/workflow_inputs/input_fields/model_field.tsx +++ b/public/pages/workflow_detail/workflow_inputs/input_fields/model_field.tsx @@ -124,13 +124,15 @@ export function ModelField(props: ModelFieldProps) { labelAppend={ props.modelCategory ? ( <ModelInfoPopover modelCategory={props.modelCategory} /> - ) : <EuiText size="xs"> - <EuiLink href={ML_CHOOSE_MODEL_LINK} target="_blank"> - Learn more - </EuiLink> - </EuiText> + ) : ( + <EuiText size="xs"> + <EuiLink href={ML_CHOOSE_MODEL_LINK} target="_blank"> + Learn more + </EuiLink> + </EuiText> + ) } - helpText={props.helpText || 'The model ID.'} + helpText={props.helpText} isInvalid={isInvalid} error={props.showError && getIn(errors, `${field.name}.id`)} > @@ -142,33 +144,33 @@ export function ModelField(props: ModelFieldProps) { disabled={isEmpty(deployedModels)} options={deployedModels.map( (option) => - ({ - value: option.id, - inputDisplay: ( - <> - <EuiText size="s">{option.name}</EuiText> - </> - ), - dropdownDisplay: ( - <> - <EuiHealth - color={ - isEmpty(option.interface) - ? 'warning' - : 'success' - } - > + ({ + value: option.id, + inputDisplay: ( + <> <EuiText size="s">{option.name}</EuiText> - </EuiHealth> - <EuiText size="xs" color="subdued"> - {isEmpty(option.interface) - ? 'Not ready - no model interface' - : 'Deployed'} - </EuiText> - </> - ), - disabled: false, - } as EuiSuperSelectOption<string>) + </> + ), + dropdownDisplay: ( + <> + <EuiHealth + color={ + isEmpty(option.interface) + ? 'warning' + : 'success' + } + > + <EuiText size="s">{option.name}</EuiText> + </EuiHealth> + <EuiText size="xs" color="subdued"> + {isEmpty(option.interface) + ? 'Not ready - no model interface' + : 'Deployed'} + </EuiText> + </> + ), + disabled: false, + } as EuiSuperSelectOption<string>) )} valueOfSelected={field.value?.id || ''} onChange={(option: string) => { diff --git a/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_inputs.tsx b/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_inputs.tsx index 58b761d0..49a2958d 100644 --- a/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_inputs.tsx +++ b/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_inputs.tsx @@ -35,7 +35,6 @@ import { EMPTY_INPUT_MAP_ENTRY, WorkflowConfig, getCharacterLimitedString, - ModelInputFormField, INPUT_TRANSFORM_OPTIONS, } from '../../../../../../common'; import { @@ -47,7 +46,6 @@ import { AppState, getMappings, useAppDispatch } from '../../../../../store'; import { getDataSourceId, getObjsFromJSONLines, - parseModelInputs, sanitizeJSONPath, } from '../../../../../utils'; import { ConfigureExpressionModal, ConfigureTemplateModal } from './modals/'; @@ -108,7 +106,7 @@ export function ModelInputs(props: ModelInputsProps) { number | undefined >(undefined); - // on initial load of the models, update model interface states + // get the model interface based on the selected ID and list of known models useEffect(() => { if (!isEmpty(models)) { const modelId = getIn(values, modelFieldPath)?.id; @@ -116,7 +114,7 @@ export function ModelInputs(props: ModelInputsProps) { setModelInterface(models[modelId]?.interface); } } - }, [models]); + }, [models, getIn(values, modelFieldPath)?.id]); // persisting doc/query/index mapping fields to collect a list // of options to display in the dropdowns when configuring input / output maps @@ -217,31 +215,6 @@ export function ModelInputs(props: ModelInputsProps) { setFieldTouched(inputMapFieldPath, true); } - // The options for keys can change. We update what options are available, based - // on if there is a model interface found, and additionally filter out any - // options that are already being used in the input map, to discourage duplicate keys. - const [keyOptions, setKeyOptions] = useState<ModelInputFormField[]>([]); - useEffect(() => { - setKeyOptions(parseModelInputs(modelInterface)); - }, [modelInterface]); - useEffect(() => { - if (modelInterface !== undefined) { - const modelInputs = parseModelInputs(modelInterface); - if (getIn(values, inputMapFieldPath) !== undefined) { - const existingKeys = getIn(values, inputMapFieldPath).map( - (inputMapEntry: InputMapEntry) => inputMapEntry.key - ) as string[]; - setKeyOptions( - modelInputs.filter( - (modelInput) => !existingKeys.includes(modelInput.label) - ) - ); - } else { - setKeyOptions(modelInputs); - } - } - }, [getIn(values, inputMapFieldPath), modelInterface]); - const valueOptions = props.context === PROCESSOR_CONTEXT.INGEST ? docFields @@ -255,37 +228,38 @@ export function ModelInputs(props: ModelInputsProps) { const populatedMap = field.value?.length !== 0; return ( <> - <EuiPanel> - {props.context === PROCESSOR_CONTEXT.SEARCH_RESPONSE && ( - <> - <BooleanField - fieldPath={oneToOnePath} - label="Merge source data" - type="Switch" - inverse={true} - helpText="Merge multiple documents into a single document for model processing. To process only one document, turn off merge source data." - /> - <EuiSpacer size="s" /> - {oneToOneChanged && ( - <> - <EuiCallOut - size="s" - color="warning" - iconType={'alert'} - title={ - <EuiText size="s"> - You have changed how source data will be processed. - You may need to update any existing input values to - reflect the updated data structure. - </EuiText> - } - /> - <EuiSpacer size="s" /> - </> - )} - </> - )} - {populatedMap ? ( + {populatedMap ? ( + <EuiPanel> + {props.context === PROCESSOR_CONTEXT.SEARCH_RESPONSE && ( + <> + <BooleanField + fieldPath={oneToOnePath} + label="Merge source data" + type="Switch" + inverse={true} + helpText="Merge multiple documents into a single document for model processing. To process only one document, turn off merge source data." + /> + <EuiSpacer size="s" /> + {oneToOneChanged && ( + <> + <EuiCallOut + size="s" + color="warning" + iconType={'alert'} + title={ + <EuiText size="s"> + You have changed how source data will be + processed. You may need to update any existing + input values to reflect the updated data + structure. + </EuiText> + } + /> + <EuiSpacer size="s" /> + </> + )} + </> + )} <EuiCompressedFormRow fullWidth={true} key={inputMapFieldPath} @@ -343,20 +317,10 @@ export function ModelInputs(props: ModelInputsProps) { <EuiFlexItem> <> {/** - * We determine if there is an interface based on if there are key options or not, - * as the options would be derived from the underlying interface. - * And if so, these values should be static. - * So, we only display the static text with no mechanism to change it's value. - * Note we still allow more entries, if a user wants to override / add custom - * keys if there is some gaps in the model interface. + * If there is a model interface, display the field name. + * Otherwise, leave as a free-form text box for a user to enter manually. */} - {!isEmpty(keyOptions) && - !isEmpty( - getIn( - values, - `${inputMapFieldPath}.${idx}.key` - ) - ) ? ( + {!isEmpty(modelInterface) ? ( <EuiText size="s" style={{ marginTop: '4px' }} @@ -366,13 +330,6 @@ export function ModelInputs(props: ModelInputsProps) { `${inputMapFieldPath}.${idx}.key` )} </EuiText> - ) : !isEmpty(keyOptions) ? ( - <SelectWithCustomOptions - fieldPath={`${inputMapFieldPath}.${idx}.key`} - options={keyOptions as any[]} - placeholder={`Name`} - allowCreate={true} - /> ) : ( <TextField fullWidth={true} @@ -628,16 +585,21 @@ export function ModelInputs(props: ModelInputsProps) { )} </> </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiSmallButtonIcon - iconType={'trash'} - color="danger" - aria-label="Delete" - onClick={() => { - deleteMapEntry(field.value, idx); - }} - /> - </EuiFlexItem> + {/** + * Only allow deleting entries if no defined model interface + */} + {isEmpty(modelInterface) && ( + <EuiFlexItem grow={false}> + <EuiSmallButtonIcon + iconType={'trash'} + color="danger" + aria-label="Delete" + onClick={() => { + deleteMapEntry(field.value, idx); + }} + /> + </EuiFlexItem> + )} </> </EuiFlexGroup> </EuiFlexItem> @@ -646,33 +608,38 @@ export function ModelInputs(props: ModelInputsProps) { ); } )} - <EuiFlexItem grow={false}> - <div> - <EuiSmallButtonEmpty - style={{ marginLeft: '-8px', marginTop: '0px' }} - iconType={'plusInCircle'} - iconSide="left" - onClick={() => { - addMapEntry(field.value); - }} - > - {`Add input`} - </EuiSmallButtonEmpty> - </div> - </EuiFlexItem> + {/** + * Only allow adding entries if no defined model interface + */} + {isEmpty(modelInterface) && ( + <EuiFlexItem grow={false}> + <div> + <EuiSmallButtonEmpty + style={{ marginLeft: '-8px', marginTop: '0px' }} + iconType={'plusInCircle'} + iconSide="left" + onClick={() => { + addMapEntry(field.value); + }} + > + {`Add input`} + </EuiSmallButtonEmpty> + </div> + </EuiFlexItem> + )} </EuiFlexGroup> </EuiCompressedFormRow> - ) : ( - <EuiSmallButton - style={{ width: '100px' }} - onClick={() => { - setFieldValue(field.name, [EMPTY_INPUT_MAP_ENTRY]); - }} - > - {'Configure'} - </EuiSmallButton> - )} - </EuiPanel> + </EuiPanel> + ) : ( + <EuiSmallButton + style={{ width: '100px' }} + onClick={() => { + setFieldValue(field.name, [EMPTY_INPUT_MAP_ENTRY]); + }} + > + {'Configure'} + </EuiSmallButton> + )} </> ); }} diff --git a/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_outputs.tsx b/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_outputs.tsx index 2b28c39e..e3d443e3 100644 --- a/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_outputs.tsx +++ b/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/model_outputs.tsx @@ -7,6 +7,18 @@ import React, { useState, useEffect } from 'react'; import { Field, FieldProps, getIn, useFormikContext } from 'formik'; import { isEmpty } from 'lodash'; import { useSelector } from 'react-redux'; +import { + EuiCompressedFormRow, + EuiCompressedSuperSelect, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSmallButton, + EuiSmallButtonEmpty, + EuiSmallButtonIcon, + EuiSuperSelectOption, + EuiText, +} from '@elastic/eui'; import { IProcessorConfig, IConfigField, @@ -21,25 +33,10 @@ import { OutputMapFormValue, EMPTY_OUTPUT_MAP_ENTRY, ExpressionVar, - ModelOutputFormField, OUTPUT_TRANSFORM_OPTIONS, } from '../../../../../../common'; -import { SelectWithCustomOptions, TextField } from '../../input_fields'; - +import { TextField } from '../../input_fields'; import { AppState } from '../../../../../store'; -import { parseModelOutputs } from '../../../../../utils'; -import { - EuiCompressedFormRow, - EuiCompressedSuperSelect, - EuiFlexGroup, - EuiFlexItem, - EuiPanel, - EuiSmallButton, - EuiSmallButtonEmpty, - EuiSmallButtonIcon, - EuiSuperSelectOption, - EuiText, -} from '@elastic/eui'; import { ConfigureMultiExpressionModal } from './modals'; interface ModelOutputsProps { @@ -75,10 +72,6 @@ export function ModelOutputs(props: ModelOutputsProps) { const modelFieldPath = `${props.baseConfigPath}.${props.config.id}.${modelField.id}`; // Assuming no more than one set of output map entries. const outputMapFieldPath = `${props.baseConfigPath}.${props.config.id}.output_map.0`; - const fullResponsePath = getIn( - values, - `${props.baseConfigPath}.${props.config.id}.full_response_path` - ); // various modal states const [expressionsModalIdx, setExpressionsModalIdx] = useState< @@ -90,7 +83,7 @@ export function ModelOutputs(props: ModelOutputsProps) { ModelInterface | undefined >(undefined); - // on initial load of the models, update model interface states + // get the model interface based on the selected ID and list of known models useEffect(() => { if (!isEmpty(models)) { const modelId = getIn(values, modelFieldPath)?.id; @@ -98,7 +91,7 @@ export function ModelOutputs(props: ModelOutputsProps) { setModelInterface(models[modelId]?.interface); } } - }, [models]); + }, [models, getIn(values, modelFieldPath)?.id]); // Adding a map entry to the end of the existing arr function addMapEntry(curEntries: OutputMapFormValue): void { @@ -118,31 +111,6 @@ export function ModelOutputs(props: ModelOutputsProps) { setFieldTouched(outputMapFieldPath, true); } - // The options for keys can change. We update what options are available, based - // on if there is a model interface found, what full_response_path is, and additionally filter out any - // options that are already being used in the output map, to discourage duplicate keys. - const [keyOptions, setKeyOptions] = useState<ModelOutputFormField[]>([]); - useEffect(() => { - setKeyOptions(parseModelOutputs(modelInterface)); - }, [modelInterface]); - useEffect(() => { - if (modelInterface !== undefined && fullResponsePath === false) { - const modelOutputs = parseModelOutputs(modelInterface); - if (getIn(values, outputMapFieldPath) !== undefined) { - const existingKeys = getIn(values, outputMapFieldPath).map( - (outputMapEntry: OutputMapEntry) => outputMapEntry.key - ) as string[]; - setKeyOptions( - modelOutputs.filter( - (modelOutput) => !existingKeys.includes(modelOutput.label) - ) - ); - } else { - setKeyOptions(modelOutputs); - } - } - }, [getIn(values, outputMapFieldPath), modelInterface, fullResponsePath]); - return ( <Field name={outputMapFieldPath} key={outputMapFieldPath}> {({ field, form }: FieldProps) => { @@ -214,20 +182,10 @@ export function ModelOutputs(props: ModelOutputsProps) { <EuiFlexItem> <> {/** - * We determine if there is an interface based on if there are key options or not, - * as the options would be derived from the underlying interface. - * And if so, these values should be static. - * So, we only display the static text with no mechanism to change it's value. - * Note we still allow more entries, if a user wants to override / add custom - * keys if there is some gaps in the model interface. + * If there is a model interface, display the field name. + * Otherwise, leave as a free-form text box for a user to enter manually. */} - {!isEmpty(keyOptions) && - !isEmpty( - getIn( - values, - `${outputMapFieldPath}.${idx}.key` - ) - ) ? ( + {!isEmpty(modelInterface) ? ( <EuiText size="s" style={{ marginTop: '4px' }} @@ -237,13 +195,6 @@ export function ModelOutputs(props: ModelOutputsProps) { `${outputMapFieldPath}.${idx}.key` )} </EuiText> - ) : !isEmpty(keyOptions) ? ( - <SelectWithCustomOptions - fieldPath={`${outputMapFieldPath}.${idx}.key`} - options={keyOptions as any[]} - placeholder={`Name`} - allowCreate={true} - /> ) : ( <TextField fullWidth={true} @@ -438,16 +389,21 @@ export function ModelOutputs(props: ModelOutputsProps) { ) : undefined} </> </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiSmallButtonIcon - iconType={'trash'} - color="danger" - aria-label="Delete" - onClick={() => { - deleteMapEntry(field.value, idx); - }} - /> - </EuiFlexItem> + {/** + * Only allow deleting entries if no defined model interface + */} + {isEmpty(modelInterface) && ( + <EuiFlexItem grow={false}> + <EuiSmallButtonIcon + iconType={'trash'} + color="danger" + aria-label="Delete" + onClick={() => { + deleteMapEntry(field.value, idx); + }} + /> + </EuiFlexItem> + )} </> </EuiFlexGroup> </EuiFlexItem> @@ -456,20 +412,25 @@ export function ModelOutputs(props: ModelOutputsProps) { ); } )} - <EuiFlexItem grow={false}> - <div> - <EuiSmallButtonEmpty - style={{ marginLeft: '-8px', marginTop: '0px' }} - iconType={'plusInCircle'} - iconSide="left" - onClick={() => { - addMapEntry(field.value); - }} - > - {`Add output`} - </EuiSmallButtonEmpty> - </div> - </EuiFlexItem> + {/** + * Only allow adding entries if no defined model interface + */} + {isEmpty(modelInterface) && ( + <EuiFlexItem grow={false}> + <div> + <EuiSmallButtonEmpty + style={{ marginLeft: '-8px', marginTop: '0px' }} + iconType={'plusInCircle'} + iconSide="left" + onClick={() => { + addMapEntry(field.value); + }} + > + {`Add output`} + </EuiSmallButtonEmpty> + </div> + </EuiFlexItem> + )} </EuiFlexGroup> </EuiCompressedFormRow> </EuiPanel>