diff --git a/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs.tsx b/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs.tsx index 7281eaf9..3803a2aa 100644 --- a/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs.tsx +++ b/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs.tsx @@ -45,6 +45,7 @@ import { parseModelOutputs, } from '../../../../utils'; import { ConfigFieldList } from '../config_field_list'; +import { OverrideQueryModal } from './modals/override_query_modal'; interface MLProcessorInputsProps { uiConfig: WorkflowConfig; @@ -127,6 +128,7 @@ export function MLProcessorInputs(props: MLProcessorInputsProps) { boolean >(false); const [isPromptModalOpen, setIsPromptModalOpen] = useState(false); + const [isQueryModalOpen, setIsQueryModalOpen] = useState(false); // model interface state const [modelInterface, setModelInterface] = useState< @@ -281,6 +283,14 @@ export function MLProcessorInputs(props: MLProcessorInputsProps) { onClose={() => setIsPromptModalOpen(false)} /> )} + {isQueryModalOpen && ( + setIsQueryModalOpen(false)} + /> + )} + {props.context === PROCESSOR_CONTEXT.SEARCH_REQUEST && ( + <> + {`Override query (Optional)`} + + setIsQueryModalOpen(true)} + data-testid="overrideQueryButton" + > + Override + + + + )} {containsPromptField && ( <> setIsPromptModalOpen(true)} + data-testid="configurePromptButton" > Configure @@ -442,7 +471,17 @@ export function MLProcessorInputs(props: MLProcessorInputsProps) { optionalField.id !== 'query_template' + ) || []), + ] + : props.config.optionalFields || [] + } baseConfigPath={props.baseConfigPath} /> diff --git a/public/pages/workflow_detail/workflow_inputs/processor_inputs/modals/configure_prompt_modal.tsx b/public/pages/workflow_detail/workflow_inputs/processor_inputs/modals/configure_prompt_modal.tsx index f3bd0a0e..ad626ecd 100644 --- a/public/pages/workflow_detail/workflow_inputs/processor_inputs/modals/configure_prompt_modal.tsx +++ b/public/pages/workflow_detail/workflow_inputs/processor_inputs/modals/configure_prompt_modal.tsx @@ -94,6 +94,10 @@ export function ConfigurePromptModal(props: ConfigurePromptModalProps) { + + Configure a custom prompt template for the model. Optionally inject + dynamic model inputs into the template. + <> diff --git a/public/pages/workflow_detail/workflow_inputs/processor_inputs/modals/override_query_modal.tsx b/public/pages/workflow_detail/workflow_inputs/processor_inputs/modals/override_query_modal.tsx new file mode 100644 index 00000000..edbab46f --- /dev/null +++ b/public/pages/workflow_detail/workflow_inputs/processor_inputs/modals/override_query_modal.tsx @@ -0,0 +1,266 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; +import { useFormikContext, getIn } from 'formik'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiSmallButton, + EuiSpacer, + EuiText, + EuiPopover, + EuiCode, + EuiBasicTable, + EuiAccordion, + EuiCopy, + EuiButtonIcon, + EuiContextMenu, +} from '@elastic/eui'; +import { + IMAGE_FIELD_PATTERN, + IProcessorConfig, + LABEL_FIELD_PATTERN, + MapEntry, + MODEL_ID_PATTERN, + ModelInterface, + QUERY_IMAGE_PATTERN, + QUERY_PRESETS, + QUERY_TEXT_PATTERN, + QueryPreset, + TEXT_FIELD_PATTERN, + VECTOR_FIELD_PATTERN, + VECTOR_PATTERN, + WorkflowFormValues, +} from '../../../../../../common'; +import { parseModelOutputs } from '../../../../../utils/utils'; +import { JsonField } from '../../input_fields'; + +interface OverrideQueryModalProps { + config: IProcessorConfig; + baseConfigPath: string; + modelInterface: ModelInterface | undefined; + onClose: () => void; +} + +/** + * A modal to configure a query template & override the existing query. Can manually configure, + * include placeholder values using model outputs, and/or select from a presets library. + */ +export function OverrideQueryModal(props: OverrideQueryModalProps) { + const { values, setFieldValue, setFieldTouched } = useFormikContext< + WorkflowFormValues + >(); + + // get some current form values + const modelOutputs = parseModelOutputs(props.modelInterface); + const queryFieldPath = `${props.baseConfigPath}.${props.config.id}.query_template`; + const outputMap = getIn( + values, + `${props.baseConfigPath}.${props.config.id}.output_map` + ); + // TODO: should handle edge case of multiple output maps configured. Currently + // defaulting to prediction 0 / assuming not multiple predictions to track. + const outputMapKeys = getIn(outputMap, '0', []).map( + (mapEntry: MapEntry) => mapEntry.key + ) as string[]; + const finalModelOutputs = + outputMapKeys.length > 0 + ? outputMapKeys.map((outputMapKey) => { + return { label: outputMapKey }; + }) + : modelOutputs.map((modelOutput) => { + return { label: modelOutput.label }; + }); + + // popover states + const [presetsPopoverOpen, setPresetsPopoverOpen] = useState(false); + + return ( + + + +

{`Override query`}

+
+
+ + + Configure a custom query template to override the existing one. + Optionally inject dynamic model outputs into the new query. + + + + <> + + setPresetsPopoverOpen(!presetsPopoverOpen)} + > + Choose from a preset + + } + isOpen={presetsPopoverOpen} + closePopover={() => setPresetsPopoverOpen(false)} + anchorPosition="downLeft" + > + ({ + name: preset.name, + onClick: () => { + setFieldValue( + queryFieldPath, + preset.query + // sanitize the query preset string into valid template placeholder format, for + // any placeholder values in the query. + // for example, replacing `"{{vector}}"` with `${vector}` + .replace( + new RegExp(`"${VECTOR_FIELD_PATTERN}"`, 'g'), + `\$\{vector_field\}` + ) + .replace( + new RegExp(`"${VECTOR_PATTERN}"`, 'g'), + `\$\{vector\}` + ) + .replace( + new RegExp(`"${TEXT_FIELD_PATTERN}"`, 'g'), + `\$\{text_field\}` + ) + .replace( + new RegExp(`"${IMAGE_FIELD_PATTERN}"`, 'g'), + `\$\{image_field\}` + ) + .replace( + new RegExp(`"${LABEL_FIELD_PATTERN}"`, 'g'), + `\$\{label_field\}` + ) + .replace( + new RegExp(`"${QUERY_TEXT_PATTERN}"`, 'g'), + `\$\{query_text\}` + ) + .replace( + new RegExp(`"${QUERY_IMAGE_PATTERN}"`, 'g'), + `\$\{query_image\}` + ) + .replace( + new RegExp(`"${MODEL_ID_PATTERN}"`, 'g'), + `\$\{model_id\}` + ) + ); + setFieldTouched(queryFieldPath, true); + setPresetsPopoverOpen(false); + }, + })), + }, + ]} + /> + + + + {finalModelOutputs.length > 0 && ( + <> + + + <> + + + To use any model outputs in the query template, copy the + placeholder string directly. + + + + + + + + )} + + + + + + + Close + + +
+ ); +} + +const columns = [ + { + name: 'Name', + field: 'label', + width: '40%', + }, + { + name: 'Placeholder string', + field: 'label', + width: '50%', + render: (label: string) => ( + + {getPlaceholderString(label)} + + ), + }, + { + name: 'Actions', + field: 'label', + width: '10%', + render: (label: string) => ( + + {(copy) => ( + + )} + + ), + }, +]; + +// small util fn to get the full placeholder string to be +// inserted into the template +function getPlaceholderString(label: string) { + return `\$\{${label}\}`; +} diff --git a/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx b/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx index 305b62ca..f8087377 100644 --- a/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx +++ b/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx @@ -292,7 +292,9 @@ export function WorkflowInputs(props: WorkflowInputsProps) { ingestTemplatesDifferent || isRunningIngest; const searchBackButtonDisabled = isRunningSearch || - (isProposingNoSearchResources ? false : searchTemplatesDifferent); + (isProposingNoSearchResources || !ingestProvisioned + ? false + : searchTemplatesDifferent); const searchUndoButtonDisabled = isRunningSave || isRunningSearch ? true