From 50cd40257f239da64ec8dbcee62e7fd6eda169ed Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Mon, 16 Sep 2024 09:38:54 -0700 Subject: [PATCH] Add optional prompt template transformation in input transform (#379) * Add toggle-able prompt template transformation in input transform Signed-off-by: Tyler Ohlsen * auto toggling based on parameters empty or not; var name updates Signed-off-by: Tyler Ohlsen * remove TODO Signed-off-by: Tyler Ohlsen --------- Signed-off-by: Tyler Ohlsen --- public/configs/ml_processor.ts | 8 +- .../collapse_processor.ts | 2 + .../normalization_processor.ts | 2 + .../workflow_detail/tools/ingest/ingest.tsx | 1 + .../workflow_detail/tools/query/query.tsx | 1 + .../input_fields/json_field.tsx | 1 + .../input_transform_modal.tsx | 175 +++++++++++++++--- .../processor_inputs/ml_processor_inputs.tsx | 1 + .../output_transform_modal.tsx | 48 ++--- .../workflow_detail/workspace/workspace.tsx | 1 + 10 files changed, 188 insertions(+), 52 deletions(-) diff --git a/public/configs/ml_processor.ts b/public/configs/ml_processor.ts index bbfac637..623981ae 100644 --- a/public/configs/ml_processor.ts +++ b/public/configs/ml_processor.ts @@ -30,10 +30,6 @@ export abstract class MLProcessor extends Processor { }, ]; this.optionalFields = [ - { - id: 'description', - type: 'string', - }, { id: 'model_config', type: 'json', @@ -62,6 +58,10 @@ export abstract class MLProcessor extends Processor { id: 'tag', type: 'string', }, + { + id: 'description', + type: 'string', + }, ]; } } diff --git a/public/configs/search_response_processors/collapse_processor.ts b/public/configs/search_response_processors/collapse_processor.ts index be0decb9..ea63fcd8 100644 --- a/public/configs/search_response_processors/collapse_processor.ts +++ b/public/configs/search_response_processors/collapse_processor.ts @@ -5,6 +5,7 @@ import { PROCESSOR_TYPE } from '../../../common'; import { Processor } from '../processor'; +import { generateId } from '../../utils'; /** * The collapse processor config. Used in search flows. @@ -12,6 +13,7 @@ import { Processor } from '../processor'; export class CollapseProcessor extends Processor { constructor() { super(); + this.id = generateId('collapse_processor'); this.type = PROCESSOR_TYPE.COLLAPSE; this.name = 'Collapse Processor'; this.fields = [ diff --git a/public/configs/search_response_processors/normalization_processor.ts b/public/configs/search_response_processors/normalization_processor.ts index c56cb1a7..9ec1dc0d 100644 --- a/public/configs/search_response_processors/normalization_processor.ts +++ b/public/configs/search_response_processors/normalization_processor.ts @@ -5,6 +5,7 @@ import { PROCESSOR_TYPE } from '../../../common'; import { Processor } from '../processor'; +import { generateId } from '../../utils'; /** * The normalization processor config. Used in search flows. @@ -12,6 +13,7 @@ import { Processor } from '../processor'; export class NormalizationProcessor extends Processor { constructor() { super(); + this.id = generateId('normalization_processor'); this.type = PROCESSOR_TYPE.NORMALIZATION; this.name = 'Normalization Processor'; this.fields = []; diff --git a/public/pages/workflow_detail/tools/ingest/ingest.tsx b/public/pages/workflow_detail/tools/ingest/ingest.tsx index d4097b16..cdae1df0 100644 --- a/public/pages/workflow_detail/tools/ingest/ingest.tsx +++ b/public/pages/workflow_detail/tools/ingest/ingest.tsx @@ -29,6 +29,7 @@ export function Ingest(props: IngestProps) { setOptions={{ fontSize: '12px', autoScrollEditorIntoView: true, + wrap: true, }} tabSize={2} /> diff --git a/public/pages/workflow_detail/tools/query/query.tsx b/public/pages/workflow_detail/tools/query/query.tsx index 85e81a7c..752c0dde 100644 --- a/public/pages/workflow_detail/tools/query/query.tsx +++ b/public/pages/workflow_detail/tools/query/query.tsx @@ -29,6 +29,7 @@ export function Query(props: QueryProps) { setOptions={{ fontSize: '12px', autoScrollEditorIntoView: true, + wrap: true, }} tabSize={2} /> diff --git a/public/pages/workflow_detail/workflow_inputs/input_fields/json_field.tsx b/public/pages/workflow_detail/workflow_inputs/input_fields/json_field.tsx index e5423f53..ca49bd97 100644 --- a/public/pages/workflow_detail/workflow_inputs/input_fields/json_field.tsx +++ b/public/pages/workflow_detail/workflow_inputs/input_fields/json_field.tsx @@ -99,6 +99,7 @@ export function JsonField(props: JsonFieldProps) { highlightActiveLine: !props.readOnly, highlightSelectedWord: !props.readOnly, highlightGutterLine: !props.readOnly, + wrap: true, }} aria-label="Code Editor" tabSize={2} diff --git a/public/pages/workflow_detail/workflow_inputs/processor_inputs/input_transform_modal.tsx b/public/pages/workflow_detail/workflow_inputs/processor_inputs/input_transform_modal.tsx index 76901f96..574f951b 100644 --- a/public/pages/workflow_detail/workflow_inputs/processor_inputs/input_transform_modal.tsx +++ b/public/pages/workflow_detail/workflow_inputs/processor_inputs/input_transform_modal.tsx @@ -26,6 +26,7 @@ import { EuiCodeBlock, EuiPopoverTitle, EuiIconTip, + EuiSwitch, } from '@elastic/eui'; import { IConfigField, @@ -64,6 +65,7 @@ import { MapArrayField } from '../input_fields'; interface InputTransformModalProps { uiConfig: WorkflowConfig; config: IProcessorConfig; + baseConfigPath: string; context: PROCESSOR_CONTEXT; inputMapField: IConfigField; inputMapFieldPath: string; @@ -72,10 +74,6 @@ interface InputTransformModalProps { onClose: () => void; } -// TODO: InputTransformModal and OutputTransformModal are very similar, and can -// likely be refactored and have more reusable components. Leave as-is until the -// UI is more finalized. - /** * A modal to configure advanced JSON-to-JSON transforms into a model's expected input */ @@ -84,24 +82,32 @@ export function InputTransformModal(props: InputTransformModalProps) { const dataSourceId = getDataSourceId(); const { values } = useFormikContext(); + // various prompt states + const [viewPromptDetails, setViewPromptDetails] = useState(false); + const [viewTransformedPrompt, setViewTransformedPrompt] = useState( + false + ); + const [originalPrompt, setOriginalPrompt] = useState(''); + const [transformedPrompt, setTransformedPrompt] = useState(''); + // fetching input data state const [isFetching, setIsFetching] = useState(false); - // source input / transformed output state + // source input / transformed input state const [sourceInput, setSourceInput] = useState('[]'); - const [transformedOutput, setTransformedOutput] = useState('{}'); + const [transformedInput, setTransformedInput] = useState('{}'); // get the current input map const map = getIn(values, props.inputMapFieldPath) as MapArrayFormValue; - // selected output state - const outputOptions = map.map((_, idx) => ({ + // selected transform state + const transformOptions = map.map((_, idx) => ({ value: idx, text: `Prediction ${idx + 1}`, })) as EuiSelectOption[]; - const [selectedOutputOption, setSelectedOutputOption] = useState< + const [selectedTransformOption, setSelectedTransformOption] = useState< number | undefined - >((outputOptions[0]?.value as number) ?? undefined); + >((transformOptions[0]?.value as number) ?? undefined); // popover state containing the model interface details, if applicable const [popoverOpen, setPopoverOpen] = useState(false); @@ -115,19 +121,19 @@ export function InputTransformModal(props: InputTransformModalProps) { if ( !isEmpty(map) && !isEmpty(JSON.parse(sourceInput)) && - selectedOutputOption !== undefined + selectedTransformOption !== undefined ) { let sampleSourceInput = {}; try { sampleSourceInput = JSON.parse(sourceInput); const output = generateTransform( sampleSourceInput, - map[selectedOutputOption] + map[selectedTransformOption] ); - setTransformedOutput(customStringify(output)); + setTransformedInput(customStringify(output)); } catch {} } - }, [map, sourceInput, selectedOutputOption]); + }, [map, sourceInput, selectedTransformOption]); // hook to re-determine validity when the generated output changes // utilize Ajv JSON schema validator library. For more info/examples, see @@ -140,11 +146,44 @@ export function InputTransformModal(props: InputTransformModalProps) { const validateFn = new Ajv().compile( props.modelInterface?.input?.properties?.parameters || {} ); - setIsValid(validateFn(JSON.parse(transformedOutput))); + setIsValid(validateFn(JSON.parse(transformedInput))); } else { setIsValid(undefined); } - }, [transformedOutput]); + }, [transformedInput]); + + // hook to set the prompt if found in the model config + useEffect(() => { + const modelConfigString = getIn( + values, + `${props.baseConfigPath}.${props.config.id}.model_config` + ); + try { + const prompt = JSON.parse(modelConfigString)?.prompt; + if (!isEmpty(prompt)) { + setOriginalPrompt(prompt); + } + } catch {} + }, [ + getIn(values, `${props.baseConfigPath}.${props.config.id}.model_config`), + ]); + + // hook to set the transformed prompt, if a valid prompt found, and + // valid parameters set + useEffect(() => { + const transformedInputObj = JSON.parse(transformedInput); + if (!isEmpty(originalPrompt) && !isEmpty(transformedInputObj)) { + setTransformedPrompt( + injectValuesIntoPrompt(originalPrompt, transformedInputObj) + ); + setViewPromptDetails(true); + setViewTransformedPrompt(true); + } else { + setViewPromptDetails(false); + setViewTransformedPrompt(false); + setTransformedPrompt(originalPrompt); + } + }, [originalPrompt, transformedInput]); return ( @@ -303,6 +342,7 @@ export function InputTransformModal(props: InputTransformModalProps) { showLineNumbers: false, showGutter: false, showPrintMargin: false, + wrap: true, }} tabSize={2} /> @@ -330,15 +370,15 @@ export function InputTransformModal(props: InputTransformModalProps) { // If the map we are adding is the first one, populate the selected option to index 0 onMapAdd={(curArray) => { if (isEmpty(curArray)) { - setSelectedOutputOption(0); + setSelectedTransformOption(0); } }} // If the map we are deleting is the one we last used to test, reset the state and // default to the first map in the list. onMapDelete={(idxToDelete) => { - if (selectedOutputOption === idxToDelete) { - setSelectedOutputOption(0); - setTransformedOutput('{}'); + if (selectedTransformOption === idxToDelete) { + setSelectedTransformOption(0); + setTransformedInput('{}'); } }} /> @@ -369,15 +409,15 @@ export function InputTransformModal(props: InputTransformModalProps) { )} - {outputOptions.length <= 1 ? ( + {transformOptions.length <= 1 ? ( Transformed input ) : ( Transformed input for} - options={outputOptions} - value={selectedOutputOption} + options={transformOptions} + value={selectedTransformOption} onChange={(e) => { - setSelectedOutputOption(Number(e.target.value)); + setSelectedTransformOption(Number(e.target.value)); }} /> )} @@ -417,7 +457,7 @@ export function InputTransformModal(props: InputTransformModalProps) { theme="textmate" width="100%" height="15vh" - value={transformedOutput} + value={transformedInput} readOnly={true} setOptions={{ fontSize: '12px', @@ -425,11 +465,72 @@ export function InputTransformModal(props: InputTransformModalProps) { showLineNumbers: false, showGutter: false, showPrintMargin: false, + wrap: true, }} tabSize={2} /> + {!isEmpty(originalPrompt) && ( + + <> + + + Transformed prompt + + + setViewPromptDetails(!viewPromptDetails)} + disabled={isEmpty(JSON.parse(transformedInput))} + /> + + {isEmpty(JSON.parse(transformedInput)) && ( + + + Transformed input is empty + + + )} + + {viewPromptDetails && ( + <> + + + setViewTransformedPrompt(!viewTransformedPrompt) + } + /> + + + + )} + + + )} @@ -440,3 +541,27 @@ export function InputTransformModal(props: InputTransformModalProps) { ); } + +function injectValuesIntoPrompt( + promptString: string, + parameters: { [key: string]: string } +): string { + let finalPromptString = promptString; + // replace any parameter placeholders in the prompt with any values found in the + // parameters obj. + // we do 2 checks - one for the regular prompt, and one with "toString()" appended. + // this is required for parameters that have values as a list, for example. + Object.keys(parameters).forEach((parameterKey) => { + const parameterValue = parameters[parameterKey]; + const regex = new RegExp(`\\$\\{parameters.${parameterKey}\\}`, 'g'); + const regexWithToString = new RegExp( + `\\$\\{parameters.${parameterKey}.toString\\(\\)\\}`, + 'g' + ); + finalPromptString = finalPromptString + .replace(regex, parameterValue) + .replace(regexWithToString, parameterValue); + }); + + return finalPromptString; +} 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 bc6fa033..6f1b53ad 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 @@ -209,6 +209,7 @@ export function MLProcessorInputs(props: MLProcessorInputsProps) { (false); - // source input / transformed output state - const [sourceInput, setSourceInput] = useState('[]'); + // source output / transformed output state + const [sourceOutput, setSourceOutput] = useState('[]'); const [transformedOutput, setTransformedOutput] = useState('{}'); // get the current output map const map = getIn(values, props.outputMapFieldPath) as MapArrayFormValue; - // selected output state - const outputOptions = map.map((_, idx) => ({ + // selected transform state + const transformOptions = map.map((_, idx) => ({ value: idx, text: `Prediction ${idx + 1}`, })) as EuiSelectOption[]; - const [selectedOutputOption, setSelectedOutputOption] = useState< + const [selectedTransformOption, setSelectedTransformOption] = useState< number | undefined - >((outputOptions[0]?.value as number) ?? undefined); + >((transformOptions[0]?.value as number) ?? undefined); // hook to re-generate the transform when any inputs to the transform are updated useEffect(() => { if ( !isEmpty(map) && - !isEmpty(JSON.parse(sourceInput)) && - selectedOutputOption !== undefined + !isEmpty(JSON.parse(sourceOutput)) && + selectedTransformOption !== undefined ) { - let sampleSourceInput = {}; + let sampleSourceOutput = {}; try { - sampleSourceInput = JSON.parse(sourceInput); + sampleSourceOutput = JSON.parse(sourceOutput); const output = generateTransform( - sampleSourceInput, - map[selectedOutputOption] + sampleSourceOutput, + map[selectedTransformOption] ); setTransformedOutput(customStringify(output)); } catch {} } - }, [map, sourceInput, selectedOutputOption]); + }, [map, sourceOutput, selectedTransformOption]); return ( @@ -169,7 +169,7 @@ export function OutputTransformModal(props: OutputTransformModalProps) { if (docObjs.length > 0) { const sampleModelResult = docObjs[0]?.inference_results || {}; - setSourceInput( + setSourceOutput( customStringify(sampleModelResult) ); } @@ -226,7 +226,7 @@ export function OutputTransformModal(props: OutputTransformModalProps) { if (hits.length > 0) { const sampleModelResult = hits[0].inference_results || {}; - setSourceInput(customStringify(sampleModelResult)); + setSourceOutput(customStringify(sampleModelResult)); } }) .catch((error: any) => { @@ -250,7 +250,7 @@ export function OutputTransformModal(props: OutputTransformModalProps) { theme="textmate" width="100%" height="15vh" - value={sourceInput} + value={sourceOutput} readOnly={true} setOptions={{ fontSize: '12px', @@ -258,6 +258,7 @@ export function OutputTransformModal(props: OutputTransformModalProps) { showLineNumbers: false, showGutter: false, showPrintMargin: false, + wrap: true, }} tabSize={2} /> @@ -280,14 +281,14 @@ export function OutputTransformModal(props: OutputTransformModalProps) { // If the map we are adding is the first one, populate the selected option to index 0 onMapAdd={(curArray) => { if (isEmpty(curArray)) { - setSelectedOutputOption(0); + setSelectedTransformOption(0); } }} // If the map we are deleting is the one we last used to test, reset the state and // default to the first map in the list. onMapDelete={(idxToDelete) => { - if (selectedOutputOption === idxToDelete) { - setSelectedOutputOption(0); + if (selectedTransformOption === idxToDelete) { + setSelectedTransformOption(0); setTransformedOutput('{}'); } }} @@ -296,15 +297,15 @@ export function OutputTransformModal(props: OutputTransformModalProps) { <> - {outputOptions.length <= 1 ? ( + {transformOptions.length <= 1 ? ( Transformed output ) : ( Transformed output for} - options={outputOptions} - value={selectedOutputOption} + options={transformOptions} + value={selectedTransformOption} onChange={(e) => { - setSelectedOutputOption(Number(e.target.value)); + setSelectedTransformOption(Number(e.target.value)); }} /> )} @@ -322,6 +323,7 @@ export function OutputTransformModal(props: OutputTransformModalProps) { showLineNumbers: false, showGutter: false, showPrintMargin: false, + wrap: true, }} tabSize={2} /> diff --git a/public/pages/workflow_detail/workspace/workspace.tsx b/public/pages/workflow_detail/workspace/workspace.tsx index 9581ad22..5c618722 100644 --- a/public/pages/workflow_detail/workspace/workspace.tsx +++ b/public/pages/workflow_detail/workspace/workspace.tsx @@ -214,6 +214,7 @@ export function Workspace(props: WorkspaceProps) { setOptions={{ fontSize: '12px', autoScrollEditorIntoView: true, + wrap: true, }} tabSize={2} />