Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add optional prompt template transformation in input transform #379

Merged
merged 3 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions public/configs/ml_processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,6 @@ export abstract class MLProcessor extends Processor {
},
];
this.optionalFields = [
{
id: 'description',
type: 'string',
},
{
id: 'model_config',
type: 'json',
Expand Down Expand Up @@ -62,6 +58,10 @@ export abstract class MLProcessor extends Processor {
id: 'tag',
type: 'string',
},
{
id: 'description',
type: 'string',
},
];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@

import { PROCESSOR_TYPE } from '../../../common';
import { Processor } from '../processor';
import { generateId } from '../../utils';

/**
* The collapse processor config. Used in search flows.
*/
export class CollapseProcessor extends Processor {
constructor() {
super();
this.id = generateId('collapse_processor');
this.type = PROCESSOR_TYPE.COLLAPSE;
this.name = 'Collapse Processor';
this.fields = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@

import { PROCESSOR_TYPE } from '../../../common';
import { Processor } from '../processor';
import { generateId } from '../../utils';

/**
* The normalization processor config. Used in search flows.
*/
export class NormalizationProcessor extends Processor {
constructor() {
super();
this.id = generateId('normalization_processor');
this.type = PROCESSOR_TYPE.NORMALIZATION;
this.name = 'Normalization Processor';
this.fields = [];
Expand Down
1 change: 1 addition & 0 deletions public/pages/workflow_detail/tools/ingest/ingest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export function Ingest(props: IngestProps) {
setOptions={{
fontSize: '12px',
autoScrollEditorIntoView: true,
wrap: true,
}}
tabSize={2}
/>
Expand Down
1 change: 1 addition & 0 deletions public/pages/workflow_detail/tools/query/query.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export function Query(props: QueryProps) {
setOptions={{
fontSize: '12px',
autoScrollEditorIntoView: true,
wrap: true,
}}
tabSize={2}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
EuiCodeBlock,
EuiPopoverTitle,
EuiIconTip,
EuiSwitch,
} from '@elastic/eui';
import {
IConfigField,
Expand Down Expand Up @@ -64,6 +65,7 @@ import { MapArrayField } from '../input_fields';
interface InputTransformModalProps {
uiConfig: WorkflowConfig;
config: IProcessorConfig;
baseConfigPath: string;
context: PROCESSOR_CONTEXT;
inputMapField: IConfigField;
inputMapFieldPath: string;
Expand All @@ -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
*/
Expand All @@ -84,24 +82,32 @@ export function InputTransformModal(props: InputTransformModalProps) {
const dataSourceId = getDataSourceId();
const { values } = useFormikContext<WorkflowFormValues>();

// various prompt states
const [viewPromptDetails, setViewPromptDetails] = useState<boolean>(false);
const [viewTransformedPrompt, setViewTransformedPrompt] = useState<boolean>(
false
);
const [originalPrompt, setOriginalPrompt] = useState<string>('');
const [transformedPrompt, setTransformedPrompt] = useState<string>('');

// fetching input data state
const [isFetching, setIsFetching] = useState<boolean>(false);

// source input / transformed output state
// source input / transformed input state
const [sourceInput, setSourceInput] = useState<string>('[]');
const [transformedOutput, setTransformedOutput] = useState<string>('{}');
const [transformedInput, setTransformedInput] = useState<string>('{}');

// 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<boolean>(false);
Expand All @@ -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
Expand All @@ -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 (
<EuiModal onClose={props.onClose} style={{ width: '70vw' }}>
Expand Down Expand Up @@ -303,6 +342,7 @@ export function InputTransformModal(props: InputTransformModalProps) {
showLineNumbers: false,
showGutter: false,
showPrintMargin: false,
wrap: true,
}}
tabSize={2}
/>
Expand Down Expand Up @@ -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('{}');
}
}}
/>
Expand Down Expand Up @@ -369,15 +409,15 @@ export function InputTransformModal(props: InputTransformModalProps) {
</EuiFlexItem>
)}
<EuiFlexItem grow={true}>
{outputOptions.length <= 1 ? (
{transformOptions.length <= 1 ? (
<EuiText>Transformed input</EuiText>
) : (
<EuiCompressedSelect
prepend={<EuiText>Transformed input for</EuiText>}
options={outputOptions}
value={selectedOutputOption}
options={transformOptions}
value={selectedTransformOption}
onChange={(e) => {
setSelectedOutputOption(Number(e.target.value));
setSelectedTransformOption(Number(e.target.value));
}}
/>
)}
Expand Down Expand Up @@ -417,19 +457,80 @@ export function InputTransformModal(props: InputTransformModalProps) {
theme="textmate"
width="100%"
height="15vh"
value={transformedOutput}
value={transformedInput}
readOnly={true}
setOptions={{
fontSize: '12px',
autoScrollEditorIntoView: true,
showLineNumbers: false,
showGutter: false,
showPrintMargin: false,
wrap: true,
}}
tabSize={2}
/>
</>
</EuiFlexItem>
{!isEmpty(originalPrompt) && (
<EuiFlexItem>
<>
<EuiFlexGroup direction="row">
<EuiFlexItem grow={false}>
<EuiText>Transformed prompt</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false} style={{ marginTop: '16px' }}>
<EuiSwitch
label="Show"
checked={viewPromptDetails}
onChange={() => setViewPromptDetails(!viewPromptDetails)}
disabled={isEmpty(JSON.parse(transformedInput))}
/>
</EuiFlexItem>
{isEmpty(JSON.parse(transformedInput)) && (
<EuiFlexItem grow={false} style={{ marginTop: '16px' }}>
<EuiText size="s" color="subdued">
Transformed input is empty
</EuiText>
</EuiFlexItem>
)}
</EuiFlexGroup>
{viewPromptDetails && (
<>
<EuiSpacer size="s" />
<EuiSwitch
label="With transformed inputs"
checked={viewTransformedPrompt}
onChange={() =>
setViewTransformedPrompt(!viewTransformedPrompt)
}
/>
<EuiSpacer size="m" />
<EuiCodeEditor
mode="json"
theme="textmate"
width="100%"
height="15vh"
value={
viewTransformedPrompt
? transformedPrompt
: originalPrompt
}
readOnly={true}
setOptions={{
fontSize: '12px',
autoScrollEditorIntoView: true,
showLineNumbers: false,
showGutter: false,
showPrintMargin: false,
wrap: true,
}}
tabSize={2}
/>
</>
)}
</>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiModalBody>
<EuiModalFooter>
Expand All @@ -440,3 +541,27 @@ export function InputTransformModal(props: InputTransformModalProps) {
</EuiModal>
);
}

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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ export function MLProcessorInputs(props: MLProcessorInputsProps) {
<InputTransformModal
uiConfig={props.uiConfig}
config={props.config}
baseConfigPath={props.baseConfigPath}
context={props.context}
inputMapField={inputMapField}
inputMapFieldPath={inputMapFieldPath}
Expand Down
Loading
Loading