Skip to content

Commit

Permalink
Automatically populate model inputs/outputs if applicable (#254) (#255)
Browse files Browse the repository at this point in the history
Signed-off-by: Tyler Ohlsen <[email protected]>
(cherry picked from commit f17ba6a)

Co-authored-by: Tyler Ohlsen <[email protected]>
  • Loading branch information
opensearch-trigger-bot[bot] and ohltyler committed Jul 31, 2024
1 parent 72998b8 commit 40923cd
Show file tree
Hide file tree
Showing 12 changed files with 373 additions and 117 deletions.
21 changes: 21 additions & 0 deletions common/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,12 +362,33 @@ export type ModelConfig = {
embeddingDimension?: number;
};

// Based off of JSONSchema. For more info, see https://json-schema.org/understanding-json-schema/reference/type
export type ModelInput = {
type: string;
description?: string;
};

export type ModelOutput = ModelInput;

// For rendering options, we extract the name (the key in the input/output obj) and combine into a single obj
export type ModelInputFormField = ModelInput & {
label: string;
};

export type ModelOutputFormField = ModelInputFormField;

export type ModelInterface = {
input: { [key: string]: ModelInput };
output: { [key: string]: ModelOutput };
};

export type Model = {
id: string;
name: string;
algorithm: MODEL_ALGORITHM;
state: MODEL_STATE;
modelConfig?: ModelConfig;
interface?: ModelInterface;
};

export type ModelDict = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export function ConfigFieldList(props: ConfigFieldListProps) {
// Default to ID if no optional formatted / prettified label provided
label={field.label || field.id}
fieldPath={`${props.baseConfigPath}.${configId}.${field.id}`}
showError={true}
onFormChange={props.onFormChange}
/>
<EuiSpacer size={CONFIG_FIELD_SPACER_SIZE} />
Expand All @@ -56,19 +57,6 @@ export function ConfigFieldList(props: ConfigFieldListProps) {
);
break;
}
case 'model': {
el = (
<EuiFlexItem key={idx}>
<ModelField
field={field}
fieldPath={`${props.baseConfigPath}.${configId}.${field.id}`}
onFormChange={props.onFormChange}
/>
<EuiSpacer size={CONFIG_FIELD_SPACER_SIZE} />
</EuiFlexItem>
);
break;
}
}
return el;
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ interface MapArrayFieldProps {
onFormChange: () => void;
onMapAdd?: (curArray: MapArrayFormValue) => void;
onMapDelete?: (idxToDelete: number) => void;
keyOptions?: any[];
valueOptions?: any[];
}

/**
Expand Down Expand Up @@ -122,6 +124,8 @@ export function MapArrayField(props: MapArrayFieldProps) {
keyPlaceholder={props.keyPlaceholder}
valuePlaceholder={props.valuePlaceholder}
onFormChange={props.onFormChange}
keyOptions={props.keyOptions}
valueOptions={props.valueOptions}
/>
</EuiPanel>
</EuiAccordion>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,20 @@ import {
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiFormControlLayoutDelimited,
EuiFormRow,
EuiIcon,
EuiLink,
EuiText,
} from '@elastic/eui';
import { Field, FieldProps, getIn, useFormikContext } from 'formik';
import { isEmpty } from 'lodash';
import {
MapEntry,
MapFormValue,
WorkflowFormValues,
} from '../../../../../common';
import { SelectWithCustomOptions } from './select_with_custom_options';
import { TextField } from './text_field';

interface MapFieldProps {
fieldPath: string; // the full path in string-form to the field (e.g., 'ingest.enrich.processors.text_embedding_processor.inputField')
Expand All @@ -29,10 +32,14 @@ interface MapFieldProps {
keyPlaceholder?: string;
valuePlaceholder?: string;
onFormChange: () => void;
keyOptions?: any[];
valueOptions?: any[];
}

/**
* Input component for configuring field mappings
* Input component for configuring field mappings. Input forms are defaulted to text fields. If
* keyOptions or valueOptions are set, set the respective input form as a select field, with those options.
* Allow custom options as a backup/default to ensure flexibility.
*/
export function MapField(props: MapFieldProps) {
const { setFieldValue, setFieldTouched, errors, touched } = useFormikContext<
Expand Down Expand Up @@ -96,47 +103,56 @@ export function MapField(props: MapFieldProps) {
<EuiFlexItem key={idx}>
<EuiFlexGroup direction="row">
<EuiFlexItem grow={true}>
<EuiFormControlLayoutDelimited
fullWidth={true}
startControl={
<input
type="string"
placeholder={props.keyPlaceholder || 'Input'}
className="euiFieldText"
value={mapping.key}
onChange={(e) => {
form.setFieldTouched(
`${props.fieldPath}.${idx}.key`,
true
);
form.setFieldValue(
`${props.fieldPath}.${idx}.key`,
e.target.value
);
props.onFormChange();
}}
/>
}
endControl={
<input
type="string"
placeholder={props.valuePlaceholder || 'Output'}
className="euiFieldText"
value={mapping.value}
onChange={(e) => {
form.setFieldTouched(
`${props.fieldPath}.${idx}.value`,
true
);
form.setFieldValue(
`${props.fieldPath}.${idx}.value`,
e.target.value
);
props.onFormChange();
}}
/>
}
/>
<EuiFlexGroup direction="row" gutterSize="xs">
<EuiFlexItem>
<>
{!isEmpty(props.keyOptions) ? (
<SelectWithCustomOptions
fieldPath={`${props.fieldPath}.${idx}.key`}
options={props.keyOptions as any[]}
placeholder={props.keyPlaceholder || 'Input'}
onFormChange={props.onFormChange}
/>
) : (
<TextField
fieldPath={`${props.fieldPath}.${idx}.key`}
placeholder={props.keyPlaceholder || 'Input'}
showError={false}
onFormChange={props.onFormChange}
/>
)}
</>
</EuiFlexItem>
<EuiFlexItem
grow={false}
style={{ marginTop: '14px' }}
>
<EuiIcon type="sortRight" />
</EuiFlexItem>
<EuiFlexItem>
<>
{!isEmpty(props.valueOptions) ? (
<SelectWithCustomOptions
fieldPath={`${props.fieldPath}.${idx}.value`}
options={props.valueOptions || []}
placeholder={
props.valuePlaceholder || 'Output'
}
onFormChange={props.onFormChange}
/>
) : (
<TextField
fieldPath={`${props.fieldPath}.${idx}.value`}
placeholder={
props.valuePlaceholder || 'Output'
}
showError={false}
onFormChange={props.onFormChange}
/>
)}
</>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import React, { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { Field, FieldProps, getIn, useFormikContext } from 'formik';
import {
EuiCallOut,
EuiFormRow,
EuiLink,
EuiSpacer,
EuiSuperSelect,
EuiSuperSelectOption,
EuiText,
Expand All @@ -25,11 +27,14 @@ import { AppState } from '../../../../store';
interface ModelFieldProps {
field: IConfigField;
fieldPath: string; // the full path in string-form to the field (e.g., 'ingest.enrich.processors.text_embedding_processor.inputField')
hasModelInterface: boolean;
onModelChange: (modelId: string) => void;
onFormChange: () => void;
}

type ModelItem = ModelFormValue & {
name: string;
interface?: {};
};

/**
Expand All @@ -56,6 +61,7 @@ export function ModelField(props: ModelFieldProps) {
id: modelId,
name: models[modelId].name,
algorithm: models[modelId].algorithm,
interface: models[modelId].interface,
} as ModelItem);
}
});
Expand All @@ -64,61 +70,75 @@ export function ModelField(props: ModelFieldProps) {
}, [models]);

return (
<Field name={props.fieldPath}>
{({ field, form }: FieldProps) => {
return (
<EuiFormRow
label={'Model'}
labelAppend={
<EuiText size="xs">
<EuiLink href={ML_CHOOSE_MODEL_LINK} target="_blank">
Learn more
</EuiLink>
</EuiText>
}
helpText={'The model ID.'}
>
<EuiSuperSelect
options={deployedModels.map(
(option) =>
({
value: option.id,
inputDisplay: (
<>
<EuiText size="s">{option.name}</EuiText>
</>
),
dropdownDisplay: (
<>
<EuiText size="s">{option.name}</EuiText>
<EuiText size="xs" color="subdued">
Deployed
</EuiText>
<EuiText size="xs" color="subdued">
{option.algorithm}
</EuiText>
</>
),
disabled: false,
} as EuiSuperSelectOption<string>)
)}
valueOfSelected={field.value?.id || ''}
onChange={(option: string) => {
form.setFieldTouched(props.fieldPath, true);
form.setFieldValue(props.fieldPath, {
id: option,
} as ModelFormValue);
props.onFormChange();
}}
isInvalid={
getIn(errors, field.name) && getIn(touched, field.name)
? true
: undefined
<>
{!props.hasModelInterface && (
<>
<EuiCallOut
size="s"
title="The selected model does not have a model interface. Cannot automatically determine model inputs and outputs."
iconType={'alert'}
color="warning"
/>
<EuiSpacer size="s" />
</>
)}
<Field name={props.fieldPath}>
{({ field, form }: FieldProps) => {
return (
<EuiFormRow
label={'Model'}
labelAppend={
<EuiText size="xs">
<EuiLink href={ML_CHOOSE_MODEL_LINK} target="_blank">
Learn more
</EuiLink>
</EuiText>
}
/>
</EuiFormRow>
);
}}
</Field>
helpText={'The model ID.'}
>
<EuiSuperSelect
options={deployedModels.map(
(option) =>
({
value: option.id,
inputDisplay: (
<>
<EuiText size="s">{option.name}</EuiText>
</>
),
dropdownDisplay: (
<>
<EuiText size="s">{option.name}</EuiText>
<EuiText size="xs" color="subdued">
Deployed
</EuiText>
<EuiText size="xs" color="subdued">
{option.algorithm}
</EuiText>
</>
),
disabled: false,
} as EuiSuperSelectOption<string>)
)}
valueOfSelected={field.value?.id || ''}
onChange={(option: string) => {
form.setFieldTouched(props.fieldPath, true);
form.setFieldValue(props.fieldPath, {
id: option,
} as ModelFormValue);
props.onFormChange();
props.onModelChange(option);
}}
isInvalid={
getIn(errors, field.name) && getIn(touched, field.name)
? true
: undefined
}
/>
</EuiFormRow>
);
}}
</Field>
</>
);
}
Loading

0 comments on commit 40923cd

Please sign in to comment.