From a7a0264cf860583d4307e05ae5ff23cc41a06100 Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Wed, 2 Oct 2024 11:34:27 -0700 Subject: [PATCH] expose prompt when applicable across contexts; support jsonpath ml ingest processor bug fix (#403) Signed-off-by: Tyler Ohlsen --- common/constants.ts | 1 + common/interfaces.ts | 8 +++- public/pages/workflow_detail/tools/tools.tsx | 4 +- .../workflow_detail/workflow_detail.test.tsx | 4 +- .../pages/workflow_detail/workflow_detail.tsx | 4 +- .../processor_inputs/ml_processor_inputs.tsx | 38 ++++++++++++++++--- .../modals/configure_prompt_modal.tsx | 18 ++++----- public/utils/utils.ts | 2 +- server/routes/helpers.ts | 2 + 9 files changed, 58 insertions(+), 23 deletions(-) diff --git a/common/constants.ts b/common/constants.ts index 6df2291f..4d03e595 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -480,3 +480,4 @@ export const EMPTY_MAP_ENTRY = { key: '', value: '' } as MapEntry; export const MODEL_OUTPUT_SCHEMA_NESTED_PATH = 'output.properties.inference_results.items.properties.output.items.properties.dataAsMap.properties'; export const MODEL_OUTPUT_SCHEMA_FULL_PATH = 'output.properties'; +export const PROMPT_FIELD = 'prompt'; // TODO: likely expand to support a pattern and/or multiple (e.g., "prompt", "prompt_template", etc.) diff --git a/common/interfaces.ts b/common/interfaces.ts index 674a21a6..b7eb7781 100644 --- a/common/interfaces.ts +++ b/common/interfaces.ts @@ -6,7 +6,12 @@ import { Node, Edge } from 'reactflow'; import { FormikValues } from 'formik'; import { ObjectSchema } from 'yup'; -import { COMPONENT_CLASS, PROCESSOR_TYPE, WORKFLOW_TYPE } from './constants'; +import { + COMPONENT_CLASS, + PROCESSOR_TYPE, + PROMPT_FIELD, + WORKFLOW_TYPE, +} from './constants'; export type Index = { name: string; @@ -401,6 +406,7 @@ export type ModelInterface = { export type ConnectorParameters = { model?: string; dimensions?: number; + [PROMPT_FIELD]?: string; }; export type Model = { diff --git a/public/pages/workflow_detail/tools/tools.tsx b/public/pages/workflow_detail/tools/tools.tsx index c11f2be0..ea324ed2 100644 --- a/public/pages/workflow_detail/tools/tools.tsx +++ b/public/pages/workflow_detail/tools/tools.tsx @@ -37,12 +37,12 @@ enum TAB_ID { const inputTabs = [ { id: TAB_ID.INGEST, - name: 'Run ingestion', + name: 'Ingest response', disabled: false, }, { id: TAB_ID.QUERY, - name: 'Run query', + name: 'Search response', disabled: false, }, { diff --git a/public/pages/workflow_detail/workflow_detail.test.tsx b/public/pages/workflow_detail/workflow_detail.test.tsx index b3a4d068..8e62cd6e 100644 --- a/public/pages/workflow_detail/workflow_detail.test.tsx +++ b/public/pages/workflow_detail/workflow_detail.test.tsx @@ -94,8 +94,8 @@ describe('WorkflowDetail Page with create ingestion option', () => { expect(getByText('Export')).toBeInTheDocument(); expect(getByText('Visual')).toBeInTheDocument(); expect(getByText('JSON')).toBeInTheDocument(); - expect(getByRole('tab', { name: 'Run ingestion' })).toBeInTheDocument(); - expect(getByRole('tab', { name: 'Run query' })).toBeInTheDocument(); + expect(getByRole('tab', { name: 'Ingest response' })).toBeInTheDocument(); + expect(getByRole('tab', { name: 'Search response' })).toBeInTheDocument(); expect(getByRole('tab', { name: 'Errors' })).toBeInTheDocument(); expect(getByRole('tab', { name: 'Resources' })).toBeInTheDocument(); diff --git a/public/pages/workflow_detail/workflow_detail.tsx b/public/pages/workflow_detail/workflow_detail.tsx index 13d6468f..a594b253 100644 --- a/public/pages/workflow_detail/workflow_detail.tsx +++ b/public/pages/workflow_detail/workflow_detail.tsx @@ -23,6 +23,7 @@ import { AppState, catIndices, getWorkflow, + searchConnectors, searchModels, useAppDispatch, } from '../../store'; @@ -102,11 +103,12 @@ export function WorkflowDetail(props: WorkflowDetailProps) { // On initial load: // - fetch workflow - // - fetch available models as their IDs may be used when building flows + // - fetch available models & connectors as their IDs may be used when building flows // - fetch all indices useEffect(() => { dispatch(getWorkflow({ workflowId, dataSourceId })); dispatch(searchModels({ apiBody: FETCH_ALL_QUERY, dataSourceId })); + dispatch(searchConnectors({ apiBody: FETCH_ALL_QUERY, dataSourceId })); dispatch(catIndices({ pattern: OMIT_SYSTEM_INDEX_PATTERN, dataSourceId })); }, []); 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 faf53875..7281eaf9 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 @@ -29,6 +29,7 @@ import { WorkflowFormValues, ModelInterface, IndexMappings, + PROMPT_FIELD, } from '../../../../../common'; import { MapArrayField, ModelField } from '../input_fields'; import { @@ -61,18 +62,18 @@ interface MLProcessorInputsProps { export function MLProcessorInputs(props: MLProcessorInputsProps) { const dispatch = useAppDispatch(); const dataSourceId = getDataSourceId(); - const models = useSelector((state: AppState) => state.ml.models); + const { models, connectors } = useSelector((state: AppState) => state.ml); const indices = useSelector((state: AppState) => state.opensearch.indices); const { values, setFieldValue, setFieldTouched } = useFormikContext< WorkflowFormValues >(); - // extracting field info from the ML processor config - // TODO: have a better mechanism for guaranteeing the expected fields/config instead of hardcoding them here + // get some current form & config values const modelField = props.config.fields.find( (field) => field.type === 'model' ) as IConfigField; const modelFieldPath = `${props.baseConfigPath}.${props.config.id}.${modelField.id}`; + const modelIdFieldPath = `${modelFieldPath}.id`; const inputMapField = props.config.fields.find( (field) => field.id === 'input_map' ) as IConfigField; @@ -88,6 +89,12 @@ export function MLProcessorInputs(props: MLProcessorInputsProps) { `${props.baseConfigPath}.${props.config.id}.full_response_path` ); + // contains a configurable prompt field or not. if so, expose some extra + // dedicated UI + const [containsPromptField, setContainsPromptField] = useState( + false + ); + // preview availability states // if there are preceding search request processors, we cannot fetch and display the interim transformed query. // additionally, cannot preview output transforms for search request processors because output_maps need to be defined @@ -140,7 +147,7 @@ export function MLProcessorInputs(props: MLProcessorInputsProps) { // on initial load of the models, update model interface states useEffect(() => { if (!isEmpty(models)) { - const modelId = getIn(values, `${modelFieldPath}.id`); + const modelId = getIn(values, modelIdFieldPath); if (modelId) { setModelInterface(models[modelId]?.interface); } @@ -212,6 +219,27 @@ export function MLProcessorInputs(props: MLProcessorInputsProps) { } }, [values?.search?.index?.name]); + // Check if there is an exposed prompt field users can override. Need to navigate + // to the associated connector details to view the connector parameters list. + useEffect(() => { + const selectedModel = Object.values(models).find( + (model) => model.id === getIn(values, modelIdFieldPath) + ); + if (selectedModel?.connectorId !== undefined) { + const connectorParameters = + connectors[selectedModel.connectorId]?.parameters; + if (connectorParameters !== undefined) { + if (connectorParameters[PROMPT_FIELD] !== undefined) { + setContainsPromptField(true); + } else { + setContainsPromptField(false); + } + } else { + setContainsPromptField(false); + } + } + }, [models, connectors, getIn(values, modelIdFieldPath)]); + return ( <> {isInputTransformModalOpen && ( @@ -262,7 +290,7 @@ export function MLProcessorInputs(props: MLProcessorInputsProps) { {!isEmpty(getIn(values, modelFieldPath)?.id) && ( <> - {props.context === PROCESSOR_CONTEXT.SEARCH_RESPONSE && ( + {containsPromptField && ( <> { - const modelConfigString = getIn( - values, - `${props.baseConfigPath}.${props.config.id}.model_config` - ) as string; try { - const prompt = JSON.parse(modelConfigString)?.prompt; + const modelConfigObj = JSON.parse(getIn(values, modelConfigPath)); + const prompt = getIn(modelConfigObj, PROMPT_FIELD); if (!isEmpty(prompt)) { setPromptStr(prompt); } else { setPromptStr(''); } } catch {} - }, [ - getIn(values, `${props.baseConfigPath}.${props.config.id}.model_config`), - ]); + }, [getIn(values, modelConfigPath)]); return ( @@ -127,7 +123,7 @@ export function ConfigurePromptModal(props: ConfigurePromptModalProps) { modelConfigPath, customStringify({ ...JSON.parse(modelConfig), - prompt: preset.prompt, + [PROMPT_FIELD]: preset.prompt, }) ); } catch {} @@ -168,9 +164,9 @@ export function ConfigurePromptModal(props: ConfigurePromptModalProps) { // if the input is blank, it is assumed the user // does not want any prompt. hence, remove the "prompt" field // from the config altogether. - delete updatedModelConfig.prompt; + delete updatedModelConfig[PROMPT_FIELD]; } else { - updatedModelConfig.prompt = promptStr; + updatedModelConfig[PROMPT_FIELD] = promptStr; } setFieldValue( modelConfigPath, diff --git a/public/utils/utils.ts b/public/utils/utils.ts index 4e24e2b7..411447b3 100644 --- a/public/utils/utils.ts +++ b/public/utils/utils.ts @@ -232,7 +232,7 @@ function getTransformedResult( ? input : mapEntry.value.startsWith(JSONPATH_ROOT_SELECTOR) ? // JSONPath transform - jsonpath.query(input, path) + jsonpath.value(input, path) : // Standard dot notation get(input, path); } diff --git a/server/routes/helpers.ts b/server/routes/helpers.ts index 87bd4e87..5ec5dd2c 100644 --- a/server/routes/helpers.ts +++ b/server/routes/helpers.ts @@ -16,6 +16,7 @@ import { ModelInterface, ModelOutput, NO_MODIFICATIONS_FOUND_TEXT, + PROMPT_FIELD, SearchHit, WORKFLOW_RESOURCE_TYPE, WORKFLOW_STATE, @@ -160,6 +161,7 @@ export function getConnectorsFromResponses( parameters: { model: connectorHit._source?.parameters?.model, dimensions: connectorHit._source?.parameters.dimensions, + [PROMPT_FIELD]: connectorHit?._source?.parameters[PROMPT_FIELD], }, } as Connector; });