From 88a151b115e83f31a938261ab9002e4acbd19d42 Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Fri, 23 Aug 2024 08:54:01 -0700 Subject: [PATCH 1/6] Add loading state on create button Signed-off-by: Tyler Ohlsen --- public/pages/workflows/new_workflow/use_case.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/public/pages/workflows/new_workflow/use_case.tsx b/public/pages/workflows/new_workflow/use_case.tsx index 1a175b1a..6d41cc20 100644 --- a/public/pages/workflows/new_workflow/use_case.tsx +++ b/public/pages/workflows/new_workflow/use_case.tsx @@ -45,6 +45,9 @@ export function UseCase(props: UseCaseProps) { processWorkflowName(props.workflow.name) ); + // is creating state + const [isCreating, setIsCreating] = useState(false); + // custom sanitization on workflow name function isInvalid(name: string): boolean { return ( @@ -83,8 +86,10 @@ export function UseCase(props: UseCaseProps) { Cancel { + setIsCreating(true); const workflowToCreate = { ...props.workflow, name: workflowName, @@ -97,6 +102,7 @@ export function UseCase(props: UseCaseProps) { ) .unwrap() .then((result) => { + setIsCreating(false); const { workflow } = result; history.replace( constructUrlWithParams( @@ -108,6 +114,7 @@ export function UseCase(props: UseCaseProps) { ); }) .catch((err: any) => { + setIsCreating(false); console.error(err); }); }} From b0ec707479f4aa3bcfc3c6ed23bb21b1d06e25bb Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Fri, 23 Aug 2024 09:03:27 -0700 Subject: [PATCH 2/6] Refactor to standalone modal component Signed-off-by: Tyler Ohlsen --- .../new_workflow/quick_configure_modal.tsx | 119 ++++++++++++++++++ .../pages/workflows/new_workflow/use_case.tsx | 106 +--------------- 2 files changed, 125 insertions(+), 100 deletions(-) create mode 100644 public/pages/workflows/new_workflow/quick_configure_modal.tsx diff --git a/public/pages/workflows/new_workflow/quick_configure_modal.tsx b/public/pages/workflows/new_workflow/quick_configure_modal.tsx new file mode 100644 index 00000000..f84c9e0d --- /dev/null +++ b/public/pages/workflows/new_workflow/quick_configure_modal.tsx @@ -0,0 +1,119 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; +import { useHistory } from 'react-router-dom'; +import { + EuiSmallButton, + EuiModal, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiModalFooter, + EuiSmallButtonEmpty, + EuiCompressedFieldText, + EuiCompressedFormRow, +} from '@elastic/eui'; +import { WORKFLOW_NAME_REGEXP, Workflow } from '../../../../common'; +import { APP_PATH } from '../../../utils'; +import { processWorkflowName } from './utils'; +import { createWorkflow, useAppDispatch } from '../../../store'; +import { constructUrlWithParams, getDataSourceId } from '../../../utils/utils'; + +interface QuickConfigureModalProps { + workflow: Workflow; + onClose(): void; +} + +export function QuickConfigureModal(props: QuickConfigureModalProps) { + const dispatch = useAppDispatch(); + const dataSourceId = getDataSourceId(); + const history = useHistory(); + + // workflow name state + const [workflowName, setWorkflowName] = useState( + processWorkflowName(props.workflow.name) + ); + + // is creating state + const [isCreating, setIsCreating] = useState(false); + + // custom sanitization on workflow name + function isInvalid(name: string): boolean { + return ( + name === '' || + name.length > 100 || + WORKFLOW_NAME_REGEXP.test(name) === false + ); + } + + return ( + props.onClose()}> + + +

{`Set a unique name for your workflow`}

+
+
+ + + { + setWorkflowName(e.target.value); + }} + /> + + + + props.onClose()}> + Cancel + + { + setIsCreating(true); + const workflowToCreate = { + ...props.workflow, + name: workflowName, + }; + dispatch( + createWorkflow({ + apiBody: workflowToCreate, + dataSourceId, + }) + ) + .unwrap() + .then((result) => { + setIsCreating(false); + const { workflow } = result; + history.replace( + constructUrlWithParams( + APP_PATH.WORKFLOWS, + + workflow.id, + dataSourceId + ) + ); + }) + .catch((err: any) => { + setIsCreating(false); + console.error(err); + }); + }} + fill={true} + color="primary" + > + Create + + +
+ ); +} diff --git a/public/pages/workflows/new_workflow/use_case.tsx b/public/pages/workflows/new_workflow/use_case.tsx index 6d41cc20..66c76725 100644 --- a/public/pages/workflows/new_workflow/use_case.tsx +++ b/public/pages/workflows/new_workflow/use_case.tsx @@ -4,7 +4,6 @@ */ import React, { useState } from 'react'; -import { useHistory } from 'react-router-dom'; import { EuiText, EuiFlexGroup, @@ -13,118 +12,25 @@ import { EuiCard, EuiHorizontalRule, EuiSmallButton, - EuiModal, - EuiModalHeader, - EuiModalHeaderTitle, - EuiModalBody, - EuiModalFooter, - EuiSmallButtonEmpty, - EuiCompressedFieldText, - EuiCompressedFormRow, } from '@elastic/eui'; -import { WORKFLOW_NAME_REGEXP, Workflow } from '../../../../common'; -import { APP_PATH } from '../../../utils'; -import { processWorkflowName } from './utils'; -import { createWorkflow, useAppDispatch } from '../../../store'; -import { constructUrlWithParams, getDataSourceId } from '../../../utils/utils'; +import { Workflow } from '../../../../common'; +import { QuickConfigureModal } from './quick_configure_modal'; interface UseCaseProps { workflow: Workflow; } export function UseCase(props: UseCaseProps) { - const dispatch = useAppDispatch(); - const dataSourceId = getDataSourceId(); - const history = useHistory(); - // name modal state const [isNameModalOpen, setIsNameModalOpen] = useState(false); - // workflow name state - const [workflowName, setWorkflowName] = useState( - processWorkflowName(props.workflow.name) - ); - - // is creating state - const [isCreating, setIsCreating] = useState(false); - - // custom sanitization on workflow name - function isInvalid(name: string): boolean { - return ( - name === '' || - name.length > 100 || - WORKFLOW_NAME_REGEXP.test(name) === false - ); - } - return ( <> {isNameModalOpen && ( - setIsNameModalOpen(false)}> - - -

{`Set a unique name for your workflow`}

-
-
- - - { - setWorkflowName(e.target.value); - }} - /> - - - - setIsNameModalOpen(false)}> - Cancel - - { - setIsCreating(true); - const workflowToCreate = { - ...props.workflow, - name: workflowName, - }; - dispatch( - createWorkflow({ - apiBody: workflowToCreate, - dataSourceId, - }) - ) - .unwrap() - .then((result) => { - setIsCreating(false); - const { workflow } = result; - history.replace( - constructUrlWithParams( - APP_PATH.WORKFLOWS, - - workflow.id, - dataSourceId - ) - ); - }) - .catch((err: any) => { - setIsCreating(false); - console.error(err); - }); - }} - fill={true} - color="primary" - > - Create - - -
+ setIsNameModalOpen(false)} + /> )} Date: Fri, 23 Aug 2024 12:28:38 -0700 Subject: [PATCH 3/6] Add basic simple form for model id for semantic/hybrid search Signed-off-by: Tyler Ohlsen --- common/constants.ts | 38 +++--- common/interfaces.ts | 4 + .../workflows/new_workflow/new_workflow.tsx | 21 +++- .../new_workflow/quick_configure_inputs.tsx | 119 ++++++++++++++++++ .../new_workflow/quick_configure_modal.tsx | 100 ++++++++++++++- 5 files changed, 257 insertions(+), 25 deletions(-) create mode 100644 public/pages/workflows/new_workflow/quick_configure_inputs.tsx diff --git a/common/constants.ts b/common/constants.ts index f63577f5..b30be6bd 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -146,8 +146,14 @@ export const DELIMITER_OPTIONAL_FIELDS = ['delimiter']; export const SHARED_OPTIONAL_FIELDS = ['max_chunk_limit', 'description', 'tag']; /** - * QUERIES + * QUERY PRESETS */ +export const VECTOR_FIELD_PATTERN = `{{vector_field}}`; +export const TEXT_FIELD_PATTERN = `{{text_field}}`; +export const QUERY_TEXT_PATTERN = `{{query_text}}`; +export const QUERY_IMAGE_PATTERN = `{{query_image}}`; +export const MODEL_ID_PATTERN = `{{model_id}}`; + export const FETCH_ALL_QUERY = { query: { match_all: {}, @@ -156,13 +162,13 @@ export const FETCH_ALL_QUERY = { }; export const SEMANTIC_SEARCH_QUERY = { _source: { - excludes: [`{{vector_field}}`], + excludes: [VECTOR_FIELD_PATTERN], }, query: { neural: { - [`{{vector_field}}`]: { - query_text: `{{query_text}}`, - model_id: `{{model_id}}`, + [VECTOR_FIELD_PATTERN]: { + query_text: QUERY_TEXT_PATTERN, + model_id: MODEL_ID_PATTERN, k: 100, }, }, @@ -170,14 +176,14 @@ export const SEMANTIC_SEARCH_QUERY = { }; export const MULTIMODAL_SEARCH_QUERY = { _source: { - excludes: [`{{vector_field}}`], + excludes: [VECTOR_FIELD_PATTERN], }, query: { neural: { - [`{{vector_field}}`]: { - query_text: `{{query_text}}`, - query_image: `{{query_image}}`, - model_id: `{{model_id}}`, + [VECTOR_FIELD_PATTERN]: { + query_text: QUERY_TEXT_PATTERN, + query_image: QUERY_IMAGE_PATTERN, + model_id: MODEL_ID_PATTERN, k: 100, }, }, @@ -185,23 +191,23 @@ export const MULTIMODAL_SEARCH_QUERY = { }; export const HYBRID_SEARCH_QUERY = { _source: { - excludes: [`{{vector_field}}`], + excludes: [VECTOR_FIELD_PATTERN], }, query: { hybrid: { queries: [ { match: { - [`{{text_field}}`]: { - query: `{{query_text}}`, + [TEXT_FIELD_PATTERN]: { + query: QUERY_TEXT_PATTERN, }, }, }, { neural: { - [`{{vector_field}}`]: { - query_text: `{{query_text}}`, - model_id: `{{model_id}}`, + [VECTOR_FIELD_PATTERN]: { + query_text: QUERY_TEXT_PATTERN, + model_id: MODEL_ID_PATTERN, k: 5, }, }, diff --git a/common/interfaces.ts b/common/interfaces.ts index 6128c6fe..84a608d6 100644 --- a/common/interfaces.ts +++ b/common/interfaces.ts @@ -461,6 +461,10 @@ export type QueryPreset = { query: string; }; +export type QuickConfigureFields = { + embeddingModelId?: string; +}; + /** ********** OPENSEARCH TYPES/INTERFACES ************ */ diff --git a/public/pages/workflows/new_workflow/new_workflow.tsx b/public/pages/workflows/new_workflow/new_workflow.tsx index f3d29762..2ae8208f 100644 --- a/public/pages/workflows/new_workflow/new_workflow.tsx +++ b/public/pages/workflows/new_workflow/new_workflow.tsx @@ -14,9 +14,19 @@ import { } from '@elastic/eui'; import { useSelector } from 'react-redux'; import { UseCase } from './use_case'; -import { Workflow, WorkflowTemplate } from '../../../../common'; -import { AppState, useAppDispatch, getWorkflowPresets } from '../../../store'; +import { + FETCH_ALL_QUERY, + Workflow, + WorkflowTemplate, +} from '../../../../common'; +import { + AppState, + useAppDispatch, + getWorkflowPresets, + searchModels, +} from '../../../store'; import { enrichPresetWorkflowWithUiMetadata } from './utils'; +import { getDataSourceId } from '../../../utils'; interface NewWorkflowProps {} @@ -27,6 +37,7 @@ interface NewWorkflowProps {} */ export function NewWorkflow(props: NewWorkflowProps) { const dispatch = useAppDispatch(); + const dataSourceId = getDataSourceId(); // workflows state const { presetWorkflows, loading } = useSelector( @@ -43,9 +54,13 @@ export function NewWorkflow(props: NewWorkflowProps) { setSearchQuery(query); }, 200); - // initial state + // on initial load: + // 1. fetch the workflow presets persisted on server-side + // 2. fetch the ML models. these may be used in quick-create views when selecting a preset, + // so we optimize by fetching once at the top-level here. useEffect(() => { dispatch(getWorkflowPresets()); + dispatch(searchModels({ apiBody: FETCH_ALL_QUERY, dataSourceId })); }, []); // initial hook to populate all workflows diff --git a/public/pages/workflows/new_workflow/quick_configure_inputs.tsx b/public/pages/workflows/new_workflow/quick_configure_inputs.tsx new file mode 100644 index 00000000..83d0d7d8 --- /dev/null +++ b/public/pages/workflows/new_workflow/quick_configure_inputs.tsx @@ -0,0 +1,119 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState, useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import { + EuiCompressedFormRow, + EuiText, + EuiSpacer, + EuiCompressedSuperSelect, + EuiSuperSelectOption, +} from '@elastic/eui'; +import { + MODEL_STATE, + Model, + QuickConfigureFields, + WORKFLOW_TYPE, +} from '../../../../common'; +import { AppState } from '../../../store'; + +interface QuickConfigureInputsProps { + workflowType?: WORKFLOW_TYPE; + setFields(fields: QuickConfigureFields): void; +} + +// Dynamic component to allow optional input configuration fields for different use cases. +// Hooks back to the parent component with such field values +export function QuickConfigureInputs(props: QuickConfigureInputsProps) { + const models = useSelector((state: AppState) => state.models.models); + + // Deployed models state + const [deployedModels, setDeployedModels] = useState([]); + + // Hook to update available deployed models + useEffect(() => { + if (models) { + setDeployedModels( + Object.values(models).filter( + (model) => model.state === MODEL_STATE.DEPLOYED + ) + ); + } + }, [models]); + + // Local field values state + const [fieldValues, setFieldValues] = useState({}); + + // Hook to update the parent field values + useEffect(() => { + props.setFields(fieldValues); + }, [fieldValues]); + + // reusable embedding model selector component + function EmbeddingModelSelector() { + return ( + + + ({ + value: option.id, + inputDisplay: ( + <> + {option.name} + + ), + dropdownDisplay: ( + <> + {option.name} + + Deployed + + + {option.algorithm} + + + ), + disabled: false, + } as EuiSuperSelectOption) + )} + valueOfSelected={fieldValues?.embeddingModelId || ''} + onChange={(option: string) => { + setFieldValues({ + ...fieldValues, + embeddingModelId: option, + }); + }} + isInvalid={false} + /> + + ); + } + + return (() => { + switch (props.workflowType) { + case WORKFLOW_TYPE.SEMANTIC_SEARCH: + case WORKFLOW_TYPE.MULTIMODAL_SEARCH: + case WORKFLOW_TYPE.HYBRID_SEARCH: { + return ( + <> + + + + ); + } + case WORKFLOW_TYPE.CUSTOM: + case undefined: + default: { + return <>; + } + } + })(); +} diff --git a/public/pages/workflows/new_workflow/quick_configure_modal.tsx b/public/pages/workflows/new_workflow/quick_configure_modal.tsx index f84c9e0d..ebec4abd 100644 --- a/public/pages/workflows/new_workflow/quick_configure_modal.tsx +++ b/public/pages/workflows/new_workflow/quick_configure_modal.tsx @@ -16,11 +16,19 @@ import { EuiCompressedFieldText, EuiCompressedFormRow, } from '@elastic/eui'; -import { WORKFLOW_NAME_REGEXP, Workflow } from '../../../../common'; +import { + MODEL_ID_PATTERN, + QuickConfigureFields, + WORKFLOW_NAME_REGEXP, + WORKFLOW_TYPE, + Workflow, +} from '../../../../common'; import { APP_PATH } from '../../../utils'; import { processWorkflowName } from './utils'; import { createWorkflow, useAppDispatch } from '../../../store'; import { constructUrlWithParams, getDataSourceId } from '../../../utils/utils'; +import { QuickConfigureInputs } from './quick_configure_inputs'; +import { isEmpty } from 'lodash'; interface QuickConfigureModalProps { workflow: Workflow; @@ -37,11 +45,15 @@ export function QuickConfigureModal(props: QuickConfigureModalProps) { processWorkflowName(props.workflow.name) ); + const [quickConfigureFields, setQuickConfigureFields] = useState< + QuickConfigureFields + >({}); + // is creating state const [isCreating, setIsCreating] = useState(false); // custom sanitization on workflow name - function isInvalid(name: string): boolean { + function isInvalidName(name: string): boolean { return ( name === '' || name.length > 100 || @@ -53,14 +65,14 @@ export function QuickConfigureModal(props: QuickConfigureModalProps) { props.onClose()}> -

{`Set a unique name for your workflow`}

+

{`Quick configure`}

+ props.onClose()}> Cancel { setIsCreating(true); - const workflowToCreate = { + let workflowToCreate = { ...props.workflow, name: workflowName, }; + if (!isEmpty(quickConfigureFields)) { + workflowToCreate = injectQuickConfigureFields( + workflowToCreate, + quickConfigureFields + ); + } dispatch( createWorkflow({ apiBody: workflowToCreate, @@ -117,3 +139,69 @@ export function QuickConfigureModal(props: QuickConfigureModalProps) {
); } + +// helper fn to populate UI config values if there are some quick configure fields available +function injectQuickConfigureFields( + workflow: Workflow, + quickConfigureFields: QuickConfigureFields +): Workflow { + if (workflow.ui_metadata?.type) { + switch (workflow.ui_metadata?.type) { + // Semantic search / hybrid search: set defaults in the ingest processor and preset query + case WORKFLOW_TYPE.SEMANTIC_SEARCH: + case WORKFLOW_TYPE.HYBRID_SEARCH: { + if ( + !isEmpty(quickConfigureFields.embeddingModelId) && + typeof quickConfigureFields.embeddingModelId === 'string' + ) { + console.log('entering injection'); + workflow = updateIngestProcessorConfig( + workflow, + quickConfigureFields.embeddingModelId + ); + workflow = updateSearchRequestConfig( + workflow, + quickConfigureFields.embeddingModelId + ); + } + } + case WORKFLOW_TYPE.CUSTOM: + case undefined: + default: + break; + } + } + return workflow; +} + +// given a model ID, update the ML processor config +function updateIngestProcessorConfig( + workflow: Workflow, + modelId: string +): Workflow { + if (workflow.ui_metadata?.config) { + workflow.ui_metadata.config.ingest.enrich.processors[0].fields.forEach( + (field) => { + if (field.id === 'model') { + field.value = { id: modelId }; + } + } + ); + } + return workflow; +} + +// given a model ID, replace placeholders in the preset query +function updateSearchRequestConfig( + workflow: Workflow, + modelId: string +): Workflow { + if (workflow.ui_metadata?.config) { + workflow.ui_metadata.config.search.request.value = ((workflow.ui_metadata + .config.search.request.value || '') as string).replace( + MODEL_ID_PATTERN, + modelId + ); + } + return workflow; +} From 179a988ddf37e56d30d31cc564e9fa98ad0e24fc Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Fri, 23 Aug 2024 15:09:23 -0700 Subject: [PATCH 4/6] Add more quick configure values Signed-off-by: Tyler Ohlsen --- common/interfaces.ts | 3 + .../search_inputs/edit_query_modal.tsx | 2 +- .../new_workflow/quick_configure_inputs.tsx | 170 +++++++++++------- .../new_workflow/quick_configure_modal.tsx | 132 ++++++++++---- 4 files changed, 209 insertions(+), 98 deletions(-) diff --git a/common/interfaces.ts b/common/interfaces.ts index 84a608d6..d8738824 100644 --- a/common/interfaces.ts +++ b/common/interfaces.ts @@ -463,6 +463,9 @@ export type QueryPreset = { export type QuickConfigureFields = { embeddingModelId?: string; + vectorField?: string; + textField?: string; + embeddingLength?: number; }; /** diff --git a/public/pages/workflow_detail/workflow_inputs/search_inputs/edit_query_modal.tsx b/public/pages/workflow_detail/workflow_inputs/search_inputs/edit_query_modal.tsx index 9d8527ae..3b00ddda 100644 --- a/public/pages/workflow_detail/workflow_inputs/search_inputs/edit_query_modal.tsx +++ b/public/pages/workflow_detail/workflow_inputs/search_inputs/edit_query_modal.tsx @@ -72,7 +72,7 @@ export function EditQueryModal(props: EditQueryModalProps) { setFieldValue(props.queryFieldPath, preset.query); setPopoverOpen(false); }, - size: 's', + size: 'full', })), }, ]} diff --git a/public/pages/workflows/new_workflow/quick_configure_inputs.tsx b/public/pages/workflows/new_workflow/quick_configure_inputs.tsx index 83d0d7d8..1a06ba78 100644 --- a/public/pages/workflows/new_workflow/quick_configure_inputs.tsx +++ b/public/pages/workflows/new_workflow/quick_configure_inputs.tsx @@ -11,6 +11,9 @@ import { EuiSpacer, EuiCompressedSuperSelect, EuiSuperSelectOption, + EuiAccordion, + EuiCompressedFieldText, + EuiCompressedFieldNumber, } from '@elastic/eui'; import { MODEL_STATE, @@ -52,68 +55,109 @@ export function QuickConfigureInputs(props: QuickConfigureInputsProps) { props.setFields(fieldValues); }, [fieldValues]); - // reusable embedding model selector component - function EmbeddingModelSelector() { - return ( - - - ({ - value: option.id, - inputDisplay: ( - <> - {option.name} - - ), - dropdownDisplay: ( - <> - {option.name} - - Deployed - - - {option.algorithm} - - - ), - disabled: false, - } as EuiSuperSelectOption) - )} - valueOfSelected={fieldValues?.embeddingModelId || ''} - onChange={(option: string) => { - setFieldValues({ - ...fieldValues, - embeddingModelId: option, - }); - }} - isInvalid={false} - /> - - ); - } - - return (() => { - switch (props.workflowType) { - case WORKFLOW_TYPE.SEMANTIC_SEARCH: - case WORKFLOW_TYPE.MULTIMODAL_SEARCH: - case WORKFLOW_TYPE.HYBRID_SEARCH: { - return ( - <> - + return ( + <> + {(props.workflowType === WORKFLOW_TYPE.SEMANTIC_SEARCH || + props.workflowType === WORKFLOW_TYPE.MULTIMODAL_SEARCH || + props.workflowType === WORKFLOW_TYPE.HYBRID_SEARCH) && ( + <> + + + + + + ({ + value: option.id, + inputDisplay: ( + <> + {option.name} + + ), + dropdownDisplay: ( + <> + {option.name} + + Deployed + + + {option.algorithm} + + + ), + disabled: false, + } as EuiSuperSelectOption) + )} + valueOfSelected={fieldValues?.embeddingModelId || ''} + onChange={(option: string) => { + setFieldValues({ + ...fieldValues, + embeddingModelId: option, + }); + }} + isInvalid={false} + /> + - - ); - } - case WORKFLOW_TYPE.CUSTOM: - case undefined: - default: { - return <>; - } - } - })(); + + { + setFieldValues({ + ...fieldValues, + textField: e.target.value, + }); + }} + /> + + + + { + setFieldValues({ + ...fieldValues, + vectorField: e.target.value, + }); + }} + /> + + + + { + setFieldValues({ + ...fieldValues, + embeddingLength: Number(e.target.value), + }); + }} + /> + + + + )} + + ); } diff --git a/public/pages/workflows/new_workflow/quick_configure_modal.tsx b/public/pages/workflows/new_workflow/quick_configure_modal.tsx index ebec4abd..e117fa05 100644 --- a/public/pages/workflows/new_workflow/quick_configure_modal.tsx +++ b/public/pages/workflows/new_workflow/quick_configure_modal.tsx @@ -18,10 +18,15 @@ import { } from '@elastic/eui'; import { MODEL_ID_PATTERN, + MapArrayFormValue, QuickConfigureFields, + TEXT_FIELD_PATTERN, + VECTOR_FIELD_PATTERN, WORKFLOW_NAME_REGEXP, WORKFLOW_TYPE, Workflow, + WorkflowConfig, + customStringify, } from '../../../../common'; import { APP_PATH } from '../../../utils'; import { processWorkflowName } from './utils'; @@ -62,7 +67,7 @@ export function QuickConfigureModal(props: QuickConfigureModalProps) { } return ( - props.onClose()}> + props.onClose()} style={{ width: '30vw' }}>

{`Quick configure`}

@@ -150,18 +155,18 @@ function injectQuickConfigureFields( // Semantic search / hybrid search: set defaults in the ingest processor and preset query case WORKFLOW_TYPE.SEMANTIC_SEARCH: case WORKFLOW_TYPE.HYBRID_SEARCH: { - if ( - !isEmpty(quickConfigureFields.embeddingModelId) && - typeof quickConfigureFields.embeddingModelId === 'string' - ) { - console.log('entering injection'); - workflow = updateIngestProcessorConfig( - workflow, - quickConfigureFields.embeddingModelId + if (!isEmpty(quickConfigureFields) && workflow.ui_metadata?.config) { + workflow.ui_metadata.config = updateIngestProcessorConfig( + workflow.ui_metadata.config, + quickConfigureFields ); - workflow = updateSearchRequestConfig( - workflow, - quickConfigureFields.embeddingModelId + workflow.ui_metadata.config = updateIndexConfig( + workflow.ui_metadata.config, + quickConfigureFields + ); + workflow.ui_metadata.config = updateSearchRequestConfig( + workflow.ui_metadata.config, + quickConfigureFields ); } } @@ -174,34 +179,93 @@ function injectQuickConfigureFields( return workflow; } -// given a model ID, update the ML processor config +// prefill ML ingest pipeline processor config, if applicable function updateIngestProcessorConfig( - workflow: Workflow, - modelId: string -): Workflow { - if (workflow.ui_metadata?.config) { - workflow.ui_metadata.config.ingest.enrich.processors[0].fields.forEach( - (field) => { - if (field.id === 'model') { - field.value = { id: modelId }; - } - } + config: WorkflowConfig, + fields: QuickConfigureFields +): WorkflowConfig { + config.ingest.enrich.processors[0].fields.forEach((field) => { + if (field.id === 'model' && fields.embeddingModelId) { + field.value = { id: fields.embeddingModelId }; + } + if (field.id === 'input_map' && fields.textField) { + field.value = [ + [{ key: '', value: fields.textField }], + ] as MapArrayFormValue; + } + if (field.id === 'output_map' && fields.vectorField) { + field.value = [ + [{ key: fields.vectorField, value: '' }], + ] as MapArrayFormValue; + } + }); + + return config; +} + +// prefill index mappings/settings, if applicable +function updateIndexConfig( + config: WorkflowConfig, + fields: QuickConfigureFields +): WorkflowConfig { + if (fields.textField) { + const existingMappings = JSON.parse( + config.ingest.index.mappings.value as string ); + config.ingest.index.mappings.value = customStringify({ + ...existingMappings, + properties: { + ...(existingMappings.properties || {}), + [fields.textField]: { + type: 'text', + }, + }, + }); } - return workflow; + if (fields.vectorField) { + const existingMappings = JSON.parse( + config.ingest.index.mappings.value as string + ); + config.ingest.index.mappings.value = customStringify({ + ...existingMappings, + properties: { + ...(existingMappings.properties || {}), + [fields.vectorField]: { + type: 'knn_vector', + dimension: fields.embeddingLength || '', + }, + }, + }); + } + return config; } -// given a model ID, replace placeholders in the preset query +// pre-populate placeholders in the query, if applicable function updateSearchRequestConfig( - workflow: Workflow, - modelId: string -): Workflow { - if (workflow.ui_metadata?.config) { - workflow.ui_metadata.config.search.request.value = ((workflow.ui_metadata - .config.search.request.value || '') as string).replace( - MODEL_ID_PATTERN, - modelId + config: WorkflowConfig, + fields: QuickConfigureFields +): WorkflowConfig { + if (fields.embeddingModelId) { + config.search.request.value = ((config.search.request.value || + '') as string).replace( + new RegExp(MODEL_ID_PATTERN, 'g'), + fields.embeddingModelId ); } - return workflow; + if (fields.textField) { + config.search.request.value = ((config.search.request.value || + '') as string).replace( + new RegExp(TEXT_FIELD_PATTERN, 'g'), + fields.textField + ); + } + if (fields.vectorField) { + config.search.request.value = ((config.search.request.value || + '') as string).replace( + new RegExp(VECTOR_FIELD_PATTERN, 'g'), + fields.vectorField + ); + } + + return config; } From 85979617f983fa8a531ab29d098de558bf1a8cee Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Fri, 23 Aug 2024 15:10:59 -0700 Subject: [PATCH 5/6] wording Signed-off-by: Tyler Ohlsen --- .../pages/workflows/new_workflow/quick_configure_inputs.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/pages/workflows/new_workflow/quick_configure_inputs.tsx b/public/pages/workflows/new_workflow/quick_configure_inputs.tsx index 1a06ba78..5233ae6f 100644 --- a/public/pages/workflows/new_workflow/quick_configure_inputs.tsx +++ b/public/pages/workflows/new_workflow/quick_configure_inputs.tsx @@ -127,7 +127,7 @@ export function QuickConfigureInputs(props: QuickConfigureInputsProps) { Date: Fri, 23 Aug 2024 15:25:58 -0700 Subject: [PATCH 6/6] Add comment Signed-off-by: Tyler Ohlsen --- .../pages/workflows/new_workflow/quick_configure_modal.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/public/pages/workflows/new_workflow/quick_configure_modal.tsx b/public/pages/workflows/new_workflow/quick_configure_modal.tsx index e117fa05..d145a4b6 100644 --- a/public/pages/workflows/new_workflow/quick_configure_modal.tsx +++ b/public/pages/workflows/new_workflow/quick_configure_modal.tsx @@ -40,6 +40,9 @@ interface QuickConfigureModalProps { onClose(): void; } +// Modal to handle workflow creation. Includes a static field to set the workflow name, and +// an optional set of quick-configure fields, that when populated, help pre-populate +// some of the detailed workflow configuration. export function QuickConfigureModal(props: QuickConfigureModalProps) { const dispatch = useAppDispatch(); const dataSourceId = getDataSourceId(); @@ -152,7 +155,8 @@ function injectQuickConfigureFields( ): Workflow { if (workflow.ui_metadata?.type) { switch (workflow.ui_metadata?.type) { - // Semantic search / hybrid search: set defaults in the ingest processor and preset query + // Semantic search / hybrid search: set defaults in the ingest processor, the index mappings, + // and the preset query case WORKFLOW_TYPE.SEMANTIC_SEARCH: case WORKFLOW_TYPE.HYBRID_SEARCH: { if (!isEmpty(quickConfigureFields) && workflow.ui_metadata?.config) {