From e3f1b3cfe13b1249fb33b8b60460b9f7adaef655 Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Wed, 17 Apr 2024 12:20:22 -0700 Subject: [PATCH 1/8] Fix router inconsistencies Signed-off-by: Tyler Ohlsen --- common/constants.ts | 2 +- public/app.tsx | 17 +++++++++++++---- .../workspace/resizable_workspace.tsx | 7 +++++-- .../pages/workflows/new_workflow/use_case.tsx | 4 ++-- public/utils/constants.ts | 2 +- 5 files changed, 22 insertions(+), 10 deletions(-) diff --git a/common/constants.ts b/common/constants.ts index fb891729..9b4563a7 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { TemplateNode, WORKFLOW_STATE } from './interfaces'; +import { WORKFLOW_STATE } from './interfaces'; export const PLUGIN_ID = 'flow-framework'; diff --git a/public/app.tsx b/public/app.tsx index 2e14044d..fa80d217 100644 --- a/public/app.tsx +++ b/public/app.tsx @@ -62,12 +62,21 @@ export const FlowFrameworkDashboardsApp = (props: Props) => { )} /> - {/* Defaulting to Workflows page */} + {/* + Defaulting to Workflows page. The pathname will need to be updated + to handle the redirection and get the router props consistent. + */} ) => ( - - )} + render={(routeProps: RouteComponentProps) => { + if (props.history.location.pathname !== APP_PATH.WORKFLOWS) { + props.history.replace({ + ...history, + pathname: APP_PATH.WORKFLOWS, + }); + } + return ; + }} /> diff --git a/public/pages/workflow_detail/workspace/resizable_workspace.tsx b/public/pages/workflow_detail/workspace/resizable_workspace.tsx index 37a37599..4120cbce 100644 --- a/public/pages/workflow_detail/workspace/resizable_workspace.tsx +++ b/public/pages/workflow_detail/workspace/resizable_workspace.tsx @@ -33,6 +33,7 @@ import { processNodes, reduceToTemplate, ReactFlowEdge, + APP_PATH, } from '../../../../common'; import { validateWorkspaceFlow, toTemplateFlows } from '../utils'; import { @@ -168,7 +169,7 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) { props.workflow && !props.workflow?.ui_metadata?.workspace_flow; const missingCachedWorkflow = props.isNewWorkflow && !props.workflow; if (missingUiFlow || missingCachedWorkflow) { - history.replace('/workflows'); + history.replace(APP_PATH.WORKFLOWS); if (missingCachedWorkflow) { getCore().notifications.toasts.addWarning('No workflow found'); } else { @@ -410,7 +411,9 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) { .unwrap() .then((result) => { const { workflow } = result; - history.replace(`/workflows/${workflow.id}`); + history.replace( + `${APP_PATH.WORKFLOWS}/${workflow.id}` + ); history.go(0); }) .catch((error: any) => { diff --git a/public/pages/workflows/new_workflow/use_case.tsx b/public/pages/workflows/new_workflow/use_case.tsx index 111c4f7c..a6eceacf 100644 --- a/public/pages/workflows/new_workflow/use_case.tsx +++ b/public/pages/workflows/new_workflow/use_case.tsx @@ -13,7 +13,7 @@ import { EuiHorizontalRule, EuiButton, } from '@elastic/eui'; -import { NEW_WORKFLOW_ID_URL, PLUGIN_ID } from '../../../../common'; +import { APP_PATH, NEW_WORKFLOW_ID_URL, PLUGIN_ID } from '../../../../common'; interface UseCaseProps { title: string; @@ -44,7 +44,7 @@ export function UseCase(props: UseCaseProps) { disabled={false} isLoading={false} onClick={props.onClick} - href={`${PLUGIN_ID}#/workflows/${NEW_WORKFLOW_ID_URL}`} + href={`${PLUGIN_ID}#${APP_PATH.WORKFLOWS}/${NEW_WORKFLOW_ID_URL}`} > Go diff --git a/public/utils/constants.ts b/public/utils/constants.ts index 0f3b75f6..74dd67b7 100644 --- a/public/utils/constants.ts +++ b/public/utils/constants.ts @@ -15,7 +15,7 @@ export enum APP_PATH { } export const BREADCRUMBS = Object.freeze({ - FLOW_FRAMEWORK: { text: 'Flow Framework', href: '#/' }, + FLOW_FRAMEWORK: { text: 'Flow Framework' }, WORKFLOWS: { text: 'Workflows', href: `#${APP_PATH.WORKFLOWS}` }, }); From 7f292819a0f437ae315cd237d852cf01ca2ebf6e Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Wed, 17 Apr 2024 17:19:35 -0700 Subject: [PATCH 2/8] Add standalone model field type Signed-off-by: Tyler Ohlsen --- common/constants.ts | 18 +- common/interfaces.ts | 47 +++- public/component_types/interfaces.ts | 2 +- .../transformer/text_embedding_transformer.ts | 9 +- .../component_details/input_field_list.tsx | 15 +- .../component_details/input_fields/index.ts | 1 + .../input_fields/model_field.tsx | 207 ++++++++++++++++++ .../input_fields/select_field.tsx | 13 +- public/utils/utils.ts | 15 ++ server/routes/helpers.ts | 26 ++- 10 files changed, 324 insertions(+), 29 deletions(-) create mode 100644 public/pages/workflow_detail/component_details/input_fields/model_field.tsx diff --git a/common/constants.ts b/common/constants.ts index 9b4563a7..fc6802a5 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { WORKFLOW_STATE } from './interfaces'; +import { PretrainedSentenceTransformer, WORKFLOW_STATE } from './interfaces'; export const PLUGIN_ID = 'flow-framework'; @@ -53,6 +53,22 @@ export const SEARCH_MODELS_NODE_API_PATH = `${BASE_MODEL_NODE_API_PATH}/search`; export const CREATE_INGEST_PIPELINE_STEP_TYPE = 'create_ingest_pipeline'; export const CREATE_INDEX_STEP_TYPE = 'create_index'; +/** + * ML PLUGIN PRETRAINED MODELS + * (based off of https://opensearch.org/docs/latest/ml-commons-plugin/pretrained-models/#sentence-transformers) + */ +export const ROBERTA_SENTENCE_TRANSFORMER = { + name: 'huggingface/sentence-transformers/all-distilroberta-v1', + shortenedName: 'all-distilroberta-v1', + vectorDimensions: 768, +} as PretrainedSentenceTransformer; + +export const BERT_SENTENCE_TRANSFORMER = { + name: 'huggingface/sentence-transformers/msmarco-distilbert-base-tas-b', + shortenedName: 'msmarco-distilbert-base-tas-b', + vectorDimensions: 768, +} as PretrainedSentenceTransformer; + /** * MISCELLANEOUS */ diff --git a/common/interfaces.ts b/common/interfaces.ts index d9c6bc43..8aeff95f 100644 --- a/common/interfaces.ts +++ b/common/interfaces.ts @@ -130,9 +130,52 @@ export enum USE_CASE { /** ********** ML PLUGIN TYPES/INTERFACES ********** */ + +// Based off of https://github.com/opensearch-project/ml-commons/blob/main/common/src/main/java/org/opensearch/ml/common/model/MLModelState.java +export enum MODEL_STATE { + REGISTERED = 'Registered', + REGISTERING = 'Registering', + DEPLOYING = 'Deploying', + DEPLOYED = 'Deployed', + PARTIALLY_DEPLOYED = 'Partially deployed', + UNDEPLOYED = 'Undeployed', + DEPLOY_FAILED = 'Deploy failed', +} + +export enum MODEL_CATEGORY { + DEPLOYED = 'Deployed', + PRETRAINED = 'Pretrained', +} + +export type PretrainedModel = { + name: string; + shortenedName: string; +}; + +export type PretrainedSentenceTransformer = PretrainedModel & { + vectorDimensions: number; +}; + +export type ModelConfig = { + modelType?: string; + embeddingDimension?: number; +}; + export type Model = { id: string; + name: string; algorithm: string; + state: MODEL_STATE; + modelConfig?: ModelConfig; +}; + +export type ModelDict = { + [modelId: string]: Model; +}; + +export type ModelFormValue = { + id: string; + category?: MODEL_CATEGORY; }; /** @@ -171,7 +214,3 @@ export enum WORKFLOW_RESOURCE_TYPE { export type WorkflowDict = { [workflowId: string]: Workflow; }; - -export type ModelDict = { - [modelId: string]: Model; -}; diff --git a/public/component_types/interfaces.ts b/public/component_types/interfaces.ts index 0056d700..055f44ff 100644 --- a/public/component_types/interfaces.ts +++ b/public/component_types/interfaces.ts @@ -10,7 +10,7 @@ import { COMPONENT_CATEGORY, COMPONENT_CLASS } from '../utils'; /** * ************ Types ************************* */ -export type FieldType = 'string' | 'json' | 'select'; +export type FieldType = 'string' | 'json' | 'select' | 'model'; export type SelectType = 'model'; export type FieldValue = string | {}; export type ComponentFormValues = FormikValues; diff --git a/public/component_types/transformer/text_embedding_transformer.ts b/public/component_types/transformer/text_embedding_transformer.ts index c856381e..4e923ce1 100644 --- a/public/component_types/transformer/text_embedding_transformer.ts +++ b/public/component_types/transformer/text_embedding_transformer.ts @@ -19,11 +19,10 @@ export class TextEmbeddingTransformer extends MLTransformer { this.inputs = []; this.createFields = [ { - label: 'Model ID', - id: 'modelId', - type: 'select', - selectType: 'model', - helpText: 'The deployed text embedding model to use for embedding.', + label: 'Model', + id: 'model', + type: 'model', + helpText: 'A text embedding model for embedding text.', helpLink: 'https://opensearch.org/docs/latest/ml-commons-plugin/integrating-ml-models/#choosing-a-model', }, diff --git a/public/pages/workflow_detail/component_details/input_field_list.tsx b/public/pages/workflow_detail/component_details/input_field_list.tsx index ca13d59c..29d0f2a6 100644 --- a/public/pages/workflow_detail/component_details/input_field_list.tsx +++ b/public/pages/workflow_detail/component_details/input_field_list.tsx @@ -5,7 +5,7 @@ import React from 'react'; import { EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { TextField, JsonField, SelectField } from './input_fields'; +import { TextField, JsonField, SelectField, ModelField } from './input_fields'; import { IComponentField } from '../../../../common'; /** @@ -54,6 +54,19 @@ export function InputFieldList(props: InputFieldListProps) { ); break; } + case 'model': { + el = ( + + + + + ); + break; + } case 'json': { el = ( diff --git a/public/pages/workflow_detail/component_details/input_fields/index.ts b/public/pages/workflow_detail/component_details/input_fields/index.ts index e2edf8bd..7d0561f5 100644 --- a/public/pages/workflow_detail/component_details/input_fields/index.ts +++ b/public/pages/workflow_detail/component_details/input_fields/index.ts @@ -6,3 +6,4 @@ export { TextField } from './text_field'; export { JsonField } from './json_field'; export { SelectField } from './select_field'; +export { ModelField } from './model_field'; diff --git a/public/pages/workflow_detail/component_details/input_fields/model_field.tsx b/public/pages/workflow_detail/component_details/input_fields/model_field.tsx new file mode 100644 index 00000000..86b5ed3b --- /dev/null +++ b/public/pages/workflow_detail/component_details/input_fields/model_field.tsx @@ -0,0 +1,207 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { Field, FieldProps, useFormikContext } from 'formik'; +import { + EuiFormRow, + EuiLink, + EuiRadioGroup, + EuiRadioGroupOption, + EuiSpacer, + EuiSuperSelect, + EuiSuperSelectOption, + EuiText, +} from '@elastic/eui'; +import { + BERT_SENTENCE_TRANSFORMER, + IComponentField, + MODEL_STATE, + ROBERTA_SENTENCE_TRANSFORMER, + WorkspaceFormValues, + isFieldInvalid, + ModelFormValue, + MODEL_CATEGORY, +} from '../../../../../common'; +import { AppState } from '../../../../store'; + +interface ModelFieldProps { + field: IComponentField; + componentId: string; + onFormChange: () => void; +} + +enum MODEL_TYPE { + CUSTOM = 'Custom', + PRETRAINED = 'Pretrained', +} + +enum RADIO_ID { + DEPLOYED = 'Deployed', + PRETRAINED = 'Pretrained', +} + +type ModelItem = ModelFormValue & { + name: string; +}; + +// TODO: there is no concrete UX for model selection and model provisioning. This component is TBD +// and simply provides the ability to select existing models, or deploy some pretrained ones, +// and persist all of this in form state. +/** + * A specific field for selecting existing deployed models, or available pretrained models. + */ +export function ModelField(props: ModelFieldProps) { + // Initial store is fetched when loading base page. We don't + // re-fetch here as it could overload client-side if user clicks back and forth / + // keeps re-rendering this component (and subsequently re-fetching data) as they're building flows + const models = useSelector((state: AppState) => state.models.models); + + const formField = `${props.componentId}.${props.field.id}`; + const { errors, touched } = useFormikContext(); + + // Deployed models state + const [deployedModels, setDeployedModels] = useState([]); + const [pretrainedModels, setPretrainedModels] = useState([]); + const [selectableModels, setSelectableModels] = useState([]); + + // Radio options state + const radioOptions = [ + { + id: MODEL_CATEGORY.DEPLOYED, + label: 'Existing deployed models', + }, + { + id: MODEL_CATEGORY.PRETRAINED, + label: 'Pretrained models', + }, + ] as EuiRadioGroupOption[]; + const [selectedRadioId, setSelectedRadioId] = useState< + MODEL_CATEGORY | undefined + >(undefined); + + // Initialize available deployed models + useEffect(() => { + if (models) { + const modelItems = [] as ModelItem[]; + Object.keys(models).forEach((modelId) => { + if (models[modelId].state === MODEL_STATE.DEPLOYED) { + modelItems.push({ + id: modelId, + name: models[modelId].name, + category: MODEL_CATEGORY.DEPLOYED, + } as ModelItem); + } + }); + setDeployedModels(modelItems); + } + }, [models]); + + // Initialize available pretrained models + useEffect(() => { + const modelItems = [ + { + id: ROBERTA_SENTENCE_TRANSFORMER.name, + name: ROBERTA_SENTENCE_TRANSFORMER.shortenedName, + category: MODEL_CATEGORY.PRETRAINED, + }, + { + id: BERT_SENTENCE_TRANSFORMER.name, + name: BERT_SENTENCE_TRANSFORMER.shortenedName, + category: MODEL_CATEGORY.PRETRAINED, + }, + ]; + setPretrainedModels(modelItems); + }, []); + + // Update the valid available models when the radio selection changes. + // e.g., only show deployed models when 'deployed' button is selected + useEffect(() => { + if (selectedRadioId !== undefined) { + if (selectedRadioId === MODEL_CATEGORY.DEPLOYED) { + setSelectableModels(deployedModels); + } else { + setSelectableModels(pretrainedModels); + } + } + }, [selectedRadioId, deployedModels, pretrainedModels]); + + return ( + + {({ field, form }: FieldProps) => { + // a hook to update the model category and trigger reloading + // of valid models to select from + useEffect(() => { + setSelectedRadioId(field.value.category); + }, [field.value.category]); + return ( + + + Learn more + + + ) : undefined + } + helpText={props.field.helpText || undefined} + > + <> + { + // if user selects a new category: + // 1. clear the saved ID + // 2. update the field category + form.setFieldValue(formField, { + id: '', + category: radioId, + } as ModelFormValue); + props.onFormChange(); + }} + > + + + ({ + value: option.name, + inputDisplay: ( + <> + {option.name} + + {option.category} + + + ), + disabled: false, + } as EuiSuperSelectOption) + )} + valueOfSelected={field.value.id || ''} + onChange={(option: string) => { + form.setFieldValue(formField, { + id: option, + category: selectedRadioId, + } as ModelFormValue); + props.onFormChange(); + }} + isInvalid={isFieldInvalid( + props.componentId, + props.field.id, + errors, + touched + )} + /> + + + ); + }} + + ); +} diff --git a/public/pages/workflow_detail/component_details/input_fields/select_field.tsx b/public/pages/workflow_detail/component_details/input_fields/select_field.tsx index 709fe18c..38017e04 100644 --- a/public/pages/workflow_detail/component_details/input_fields/select_field.tsx +++ b/public/pages/workflow_detail/component_details/input_fields/select_field.tsx @@ -19,7 +19,6 @@ import { getInitialValue, isFieldInvalid, } from '../../../../../common'; -import { AppState } from '../../../../store'; interface SelectFieldProps { field: IComponentField; @@ -32,21 +31,15 @@ interface SelectFieldProps { * options. */ export function SelectField(props: SelectFieldProps) { - // Redux store state - // Initial store is fetched when loading base page. We don't - // re-fetch here as it could overload client-side if user clicks back and forth / - // keeps re-rendering this component (and subsequently re-fetching data) as they're building flows - const models = useSelector((state: AppState) => state.models.models); - // Options state const [options, setOptions] = useState([]); // Populate options depending on the select type useEffect(() => { - if (props.field.selectType === 'model' && models) { - setOptions(Object.keys(models)); + // TODO: figure out how we want to utilize select types to customize the options + if (props.field.selectType === 'model') { } - }, [models]); + }, []); const formField = `${props.componentId}.${props.field.id}`; const { errors, touched } = useFormikContext(); diff --git a/public/utils/utils.ts b/public/utils/utils.ts index 3f9a7b8b..966202f9 100644 --- a/public/utils/utils.ts +++ b/public/utils/utils.ts @@ -18,6 +18,7 @@ import { ReactFlowComponent, Workflow, WorkflowTemplate, + ModelFormValue, } from '../../common'; // Append 16 random characters @@ -82,6 +83,7 @@ export function reduceToTemplate(workflow: Workflow): WorkflowTemplate { lastUpdated, lastLaunched, state, + resourcesCreated, ...workflowTemplate } = workflow; return workflowTemplate; @@ -96,6 +98,12 @@ export function getInitialValue(fieldType: FieldType): FieldValue { case 'select': { return ''; } + case 'model': { + return { + id: '', + category: undefined, + } as ModelFormValue; + } case 'json': { return {}; } @@ -162,6 +170,13 @@ function getFieldSchema(field: IComponentField): Schema { baseSchema = yup.string().min(1, 'Too short').max(70, 'Too long'); break; } + case 'model': { + baseSchema = yup.object().shape({ + id: yup.string().min(1, 'Too short').max(70, 'Too long').required(), + category: yup.string().required(), + }); + break; + } case 'json': { baseSchema = yup.object().json(); break; diff --git a/server/routes/helpers.ts b/server/routes/helpers.ts index b4d39991..44ed2f3b 100644 --- a/server/routes/helpers.ts +++ b/server/routes/helpers.ts @@ -6,6 +6,7 @@ import { DEFAULT_NEW_WORKFLOW_STATE_TYPE, INDEX_NOT_FOUND_EXCEPTION, + MODEL_STATE, Model, ModelDict, WORKFLOW_RESOURCE_TYPE, @@ -85,13 +86,24 @@ export function getWorkflowsFromResponses( export function getModelsFromResponses(modelHits: any[]): ModelDict { const modelDict = {} as ModelDict; modelHits.forEach((modelHit: any) => { - const modelId = modelHit._source?.model_id; - // in case of schema changes from ML plugin, this may crash. That is ok, as the error - // produced will help expose the root cause - modelDict[modelId] = { - id: modelId, - algorithm: modelHit._source?.algorithm, - } as Model; + // search model API returns hits for each deployed model chunk. ignore these hits + if (modelHit._source.chunk_number === undefined) { + const modelId = modelHit._id; + // in case of schema changes from ML plugin, this may crash. That is ok, as the error + // produced will help expose the root cause + modelDict[modelId] = { + id: modelId, + name: modelHit._source?.name, + algorithm: modelHit._source?.algorithm, + // @ts-ignore + state: MODEL_STATE[modelHit._source?.model_state], + modelConfig: { + modelType: modelHit._source?.model_config?.model_type, + embeddingDimension: + modelHit._source?.model_config?.embedding_dimension, + }, + } as Model; + } }); return modelDict; } From 15255aca9064e62ccf17276762fd9afdac3b2f04 Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Thu, 18 Apr 2024 10:55:31 -0700 Subject: [PATCH 3/8] Dynamically parse to handle deployment of pretrained models Signed-off-by: Tyler Ohlsen --- common/constants.ts | 23 ++- common/interfaces.ts | 17 ++ .../transformer/text_embedding_transformer.ts | 1 - .../component_details/component_inputs.tsx | 3 + .../input_fields/model_field.tsx | 18 +-- .../resources/resource_list.tsx | 10 +- .../utils/workflow_to_template_utils.ts | 153 +++++++++++++----- 7 files changed, 173 insertions(+), 52 deletions(-) diff --git a/common/constants.ts b/common/constants.ts index fc6802a5..87ef7960 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -3,7 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { PretrainedSentenceTransformer, WORKFLOW_STATE } from './interfaces'; +import { + PRETRAINED_MODEL_FORMAT, + PretrainedSentenceTransformer, + WORKFLOW_STATE, +} from './interfaces'; export const PLUGIN_ID = 'flow-framework'; @@ -52,6 +56,8 @@ export const SEARCH_MODELS_NODE_API_PATH = `${BASE_MODEL_NODE_API_PATH}/search`; */ export const CREATE_INGEST_PIPELINE_STEP_TYPE = 'create_ingest_pipeline'; export const CREATE_INDEX_STEP_TYPE = 'create_index'; +export const REGISTER_LOCAL_PRETRAINED_MODEL_STEP_TYPE = + 'register_local_pretrained_model'; /** * ML PLUGIN PRETRAINED MODELS @@ -60,12 +66,27 @@ export const CREATE_INDEX_STEP_TYPE = 'create_index'; export const ROBERTA_SENTENCE_TRANSFORMER = { name: 'huggingface/sentence-transformers/all-distilroberta-v1', shortenedName: 'all-distilroberta-v1', + description: 'A sentence transformer from Hugging Face', + format: PRETRAINED_MODEL_FORMAT.TORCH_SCRIPT, + version: '1.0.1', + vectorDimensions: 768, +} as PretrainedSentenceTransformer; + +export const MPNET_SENTENCE_TRANSFORMER = { + name: 'huggingface/sentence-transformers/all-mpnet-base-v2', + shortenedName: 'all-mpnet-base-v2', + description: 'A sentence transformer from Hugging Face', + format: PRETRAINED_MODEL_FORMAT.TORCH_SCRIPT, + version: '1.0.1', vectorDimensions: 768, } as PretrainedSentenceTransformer; export const BERT_SENTENCE_TRANSFORMER = { name: 'huggingface/sentence-transformers/msmarco-distilbert-base-tas-b', shortenedName: 'msmarco-distilbert-base-tas-b', + description: 'A sentence transformer from Hugging Face', + format: PRETRAINED_MODEL_FORMAT.TORCH_SCRIPT, + version: '1.0.2', vectorDimensions: 768, } as PretrainedSentenceTransformer; diff --git a/common/interfaces.ts b/common/interfaces.ts index 8aeff95f..99defd51 100644 --- a/common/interfaces.ts +++ b/common/interfaces.ts @@ -82,6 +82,16 @@ export type CreateIndexNode = TemplateNode & { }; }; +export type RegisterPretrainedModelNode = TemplateNode & { + user_inputs: { + name: string; + description: string; + model_format: string; + version: string; + deploy: boolean; + }; +}; + export type TemplateEdge = { source: string; dest: string; @@ -147,9 +157,16 @@ export enum MODEL_CATEGORY { PRETRAINED = 'Pretrained', } +export enum PRETRAINED_MODEL_FORMAT { + TORCH_SCRIPT = 'TORCH_SCRIPT', +} + export type PretrainedModel = { name: string; shortenedName: string; + description: string; + format: PRETRAINED_MODEL_FORMAT; + version: string; }; export type PretrainedSentenceTransformer = PretrainedModel & { diff --git a/public/component_types/transformer/text_embedding_transformer.ts b/public/component_types/transformer/text_embedding_transformer.ts index 4e923ce1..5f7f4ff2 100644 --- a/public/component_types/transformer/text_embedding_transformer.ts +++ b/public/component_types/transformer/text_embedding_transformer.ts @@ -35,7 +35,6 @@ export class TextEmbeddingTransformer extends MLTransformer { helpLink: 'https://opensearch.org/docs/latest/ingest-pipelines/processors/text-embedding/', }, - { label: 'Vector Field', id: 'vectorField', diff --git a/public/pages/workflow_detail/component_details/component_inputs.tsx b/public/pages/workflow_detail/component_details/component_inputs.tsx index 6e8ad9b2..2786b6a8 100644 --- a/public/pages/workflow_detail/component_details/component_inputs.tsx +++ b/public/pages/workflow_detail/component_details/component_inputs.tsx @@ -55,6 +55,9 @@ export function ComponentInputs(props: ComponentInputsProps) {

{props.selectedComponent.data.label || ''}

+ + {props.selectedComponent.data.description} + void; } -enum MODEL_TYPE { - CUSTOM = 'Custom', - PRETRAINED = 'Pretrained', -} - -enum RADIO_ID { - DEPLOYED = 'Deployed', - PRETRAINED = 'Pretrained', -} - type ModelItem = ModelFormValue & { name: string; }; @@ -108,6 +99,11 @@ export function ModelField(props: ModelFieldProps) { name: ROBERTA_SENTENCE_TRANSFORMER.shortenedName, category: MODEL_CATEGORY.PRETRAINED, }, + { + id: MPNET_SENTENCE_TRANSFORMER.name, + name: MPNET_SENTENCE_TRANSFORMER.shortenedName, + category: MODEL_CATEGORY.PRETRAINED, + }, { id: BERT_SENTENCE_TRANSFORMER.name, name: BERT_SENTENCE_TRANSFORMER.shortenedName, @@ -171,7 +167,7 @@ export function ModelField(props: ModelFieldProps) { options={selectableModels.map( (option) => ({ - value: option.name, + value: option.id, inputDisplay: ( <> {option.name} diff --git a/public/pages/workflow_detail/resources/resource_list.tsx b/public/pages/workflow_detail/resources/resource_list.tsx index 0acb0f9f..72a10f1f 100644 --- a/public/pages/workflow_detail/resources/resource_list.tsx +++ b/public/pages/workflow_detail/resources/resource_list.tsx @@ -23,10 +23,16 @@ interface ResourceListProps { export function ResourceList(props: ResourceListProps) { const [allResources, setAllResources] = useState([]); - // Hook to initialize all resources + // Hook to initialize all resources. Reduce to unique IDs, since + // the backend resources may include the same resource multiple times + // (e.g., register and deploy steps persist the same model ID resource) useEffect(() => { if (props.workflow?.resourcesCreated) { - setAllResources(props.workflow.resourcesCreated); + const resourcesMap = {} as { [id: string]: WorkflowResource }; + props.workflow.resourcesCreated.forEach((resource) => { + resourcesMap[resource.id] = resource; + }); + setAllResources(Object.values(resourcesMap)); } }, [props.workflow?.resourcesCreated]); diff --git a/public/pages/workflow_detail/utils/workflow_to_template_utils.ts b/public/pages/workflow_detail/utils/workflow_to_template_utils.ts index 3e9ce61f..881e8442 100644 --- a/public/pages/workflow_detail/utils/workflow_to_template_utils.ts +++ b/public/pages/workflow_detail/utils/workflow_to_template_utils.ts @@ -20,6 +20,14 @@ import { CreateIndexNode, TemplateFlow, TemplateEdge, + ModelFormValue, + MODEL_CATEGORY, + RegisterPretrainedModelNode, + PretrainedSentenceTransformer, + ROBERTA_SENTENCE_TRANSFORMER, + MPNET_SENTENCE_TRANSFORMER, + BERT_SENTENCE_TRANSFORMER, + REGISTER_LOCAL_PRETRAINED_MODEL_STEP_TYPE, } from '../../../../common'; /** @@ -68,13 +76,13 @@ function toProvisionTemplateFlow( edges: ReactFlowEdge[] ): TemplateFlow { const prevNodes = [] as ReactFlowComponent[]; - const templateNodes = [] as TemplateNode[]; + const finalTemplateNodes = [] as TemplateNode[]; const templateEdges = [] as TemplateEdge[]; nodes.forEach((node) => { - const templateNode = toTemplateNode(node, prevNodes, edges); + const templateNodes = toTemplateNodes(node, prevNodes, edges); // it may be undefined if the node is not convertible for some reason - if (templateNode) { - templateNodes.push(templateNode); + if (templateNodes) { + finalTemplateNodes.push(...templateNodes); prevNodes.push(node); } }); @@ -84,20 +92,20 @@ function toProvisionTemplateFlow( }); return { - nodes: templateNodes, + nodes: finalTemplateNodes, edges: templateEdges, }; } -function toTemplateNode( +function toTemplateNodes( flowNode: ReactFlowComponent, prevNodes: ReactFlowComponent[], edges: ReactFlowEdge[] -): TemplateNode | undefined { +): TemplateNode[] | undefined { if (flowNode.data.baseClasses?.includes(COMPONENT_CLASS.ML_TRANSFORMER)) { - return toIngestPipelineNode(flowNode); + return toIngestPipelineNodes(flowNode); } else if (flowNode.data.baseClasses?.includes(COMPONENT_CLASS.INDEXER)) { - return toIndexerNode(flowNode, prevNodes, edges); + return [toIndexerNode(flowNode, prevNodes, edges)]; } } @@ -110,9 +118,9 @@ function toTemplateEdge(flowEdge: ReactFlowEdge): TemplateEdge { // General fn to process all ML transform nodes. Convert into a final // ingest pipeline with a processor specific to the final class of the node. -function toIngestPipelineNode( - flowNode: ReactFlowComponent -): CreateIngestPipelineNode { +// Optionally prepend a register pretrained model step if the selected model +// is a pretrained and undeployed one. +function toIngestPipelineNodes(flowNode: ReactFlowComponent): TemplateNode[] { // TODO a few improvements to make here: // 1. Consideration of multiple ingest processors and how to collect them all, and finally create // a single ingest pipeline with all of them, in the same order as done on the UI @@ -120,34 +128,103 @@ function toIngestPipelineNode( switch (flowNode.data.type) { case COMPONENT_CLASS.TEXT_EMBEDDING_TRANSFORMER: default: { - const { modelId, inputField, vectorField } = componentDataToFormik( + const { model, inputField, vectorField } = componentDataToFormik( flowNode.data - ); + ) as { model: ModelFormValue; inputField: string; vectorField: string }; + const modelId = model.id; - return { - id: flowNode.data.id, - type: CREATE_INGEST_PIPELINE_STEP_TYPE, - user_inputs: { - // TODO: expose as customizable - pipeline_id: 'test-pipeline', - model_id: modelId, - input_field: inputField, - output_field: vectorField, - configurations: { - description: 'An ingest pipeline with a text embedding processor.', - processors: [ - { - text_embedding: { - model_id: modelId, - field_map: { - [inputField]: vectorField, - }, - }, - } as TextEmbeddingProcessor, - ], + let registerModelStep = undefined as + | RegisterPretrainedModelNode + | undefined; + if (model.category === MODEL_CATEGORY.PRETRAINED) { + const pretrainedModelMap = {} as { + [modelName: string]: PretrainedSentenceTransformer; + }; + [ + ROBERTA_SENTENCE_TRANSFORMER, + MPNET_SENTENCE_TRANSFORMER, + BERT_SENTENCE_TRANSFORMER, + ].map((transformer) => { + pretrainedModelMap[transformer.name] = transformer; + }); + // the model ID in the form will be the unique name of the pretrained model + const pretrainedModel = pretrainedModelMap[modelId]; + registerModelStep = { + id: REGISTER_LOCAL_PRETRAINED_MODEL_STEP_TYPE, + type: REGISTER_LOCAL_PRETRAINED_MODEL_STEP_TYPE, + user_inputs: { + name: pretrainedModel.name, + description: pretrainedModel.description, + model_format: pretrainedModel.format, + version: pretrainedModel.version, + deploy: true, }, - }, - }; + } as RegisterPretrainedModelNode; + } + + // If we have a register model step, add it first, and use + // the produced model ID in the ingest pipeline step + if (registerModelStep !== undefined) { + return [ + registerModelStep, + { + id: flowNode.data.id, + type: CREATE_INGEST_PIPELINE_STEP_TYPE, + previous_node_inputs: { + [REGISTER_LOCAL_PRETRAINED_MODEL_STEP_TYPE]: 'model_id', + }, + user_inputs: { + // TODO: expose as customizable + pipeline_id: 'test-pipeline', + model_id: `\${{${REGISTER_LOCAL_PRETRAINED_MODEL_STEP_TYPE}.model_id}}`, + input_field: inputField, + output_field: vectorField, + configurations: { + description: + 'An ingest pipeline with a text embedding processor.', + processors: [ + { + text_embedding: { + model_id: `\${{${REGISTER_LOCAL_PRETRAINED_MODEL_STEP_TYPE}.model_id}}`, + field_map: { + [inputField]: vectorField, + }, + }, + } as TextEmbeddingProcessor, + ], + }, + }, + } as CreateIngestPipelineNode, + ]; + } else { + return [ + { + id: flowNode.data.id, + type: CREATE_INGEST_PIPELINE_STEP_TYPE, + user_inputs: { + // TODO: expose as customizable + pipeline_id: 'test-pipeline', + model_id: modelId, + input_field: inputField, + output_field: vectorField, + configurations: { + description: + 'An ingest pipeline with a text embedding processor.', + processors: [ + { + text_embedding: { + model_id: modelId, + field_map: { + [inputField]: vectorField, + }, + }, + } as TextEmbeddingProcessor, + ], + }, + }, + } as CreateIngestPipelineNode, + ]; + } } } } @@ -191,6 +268,8 @@ function toIndexerNode( properties: { [vectorField]: { type: 'knn_vector', + // TODO: remove hardcoding, fetch from the selected model + // (existing or from pretrained configuration) dimension: 768, method: { engine: 'lucene', From e52c6aba45f6e7b3f8047751f1a8c036161246ba Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Thu, 18 Apr 2024 11:21:06 -0700 Subject: [PATCH 4/8] Add ingest pipeline field Signed-off-by: Tyler Ohlsen --- .../transformer/text_embedding_transformer.ts | 8 ++++++++ .../utils/workflow_to_template_utils.ts | 20 ++++++++++++------- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/public/component_types/transformer/text_embedding_transformer.ts b/public/component_types/transformer/text_embedding_transformer.ts index 5f7f4ff2..88707813 100644 --- a/public/component_types/transformer/text_embedding_transformer.ts +++ b/public/component_types/transformer/text_embedding_transformer.ts @@ -18,6 +18,14 @@ export class TextEmbeddingTransformer extends MLTransformer { this.baseClasses = [...this.baseClasses, this.type]; this.inputs = []; this.createFields = [ + { + label: 'Ingest Pipeline', + id: 'ingestPipelineName', + type: 'string', + helpText: + 'The name of the ingest pipeline configured with the text embedding transformer', + helpLink: 'https://opensearch.org/docs/latest/ingest-pipelines/', + }, { label: 'Model', id: 'model', diff --git a/public/pages/workflow_detail/utils/workflow_to_template_utils.ts b/public/pages/workflow_detail/utils/workflow_to_template_utils.ts index 881e8442..713b126f 100644 --- a/public/pages/workflow_detail/utils/workflow_to_template_utils.ts +++ b/public/pages/workflow_detail/utils/workflow_to_template_utils.ts @@ -128,9 +128,17 @@ function toIngestPipelineNodes(flowNode: ReactFlowComponent): TemplateNode[] { switch (flowNode.data.type) { case COMPONENT_CLASS.TEXT_EMBEDDING_TRANSFORMER: default: { - const { model, inputField, vectorField } = componentDataToFormik( - flowNode.data - ) as { model: ModelFormValue; inputField: string; vectorField: string }; + const { + ingestPipelineName, + model, + inputField, + vectorField, + } = componentDataToFormik(flowNode.data) as { + ingestPipelineName: string; + model: ModelFormValue; + inputField: string; + vectorField: string; + }; const modelId = model.id; let registerModelStep = undefined as @@ -174,8 +182,7 @@ function toIngestPipelineNodes(flowNode: ReactFlowComponent): TemplateNode[] { [REGISTER_LOCAL_PRETRAINED_MODEL_STEP_TYPE]: 'model_id', }, user_inputs: { - // TODO: expose as customizable - pipeline_id: 'test-pipeline', + pipeline_id: ingestPipelineName, model_id: `\${{${REGISTER_LOCAL_PRETRAINED_MODEL_STEP_TYPE}.model_id}}`, input_field: inputField, output_field: vectorField, @@ -202,8 +209,7 @@ function toIngestPipelineNodes(flowNode: ReactFlowComponent): TemplateNode[] { id: flowNode.data.id, type: CREATE_INGEST_PIPELINE_STEP_TYPE, user_inputs: { - // TODO: expose as customizable - pipeline_id: 'test-pipeline', + pipeline_id: ingestPipelineName, model_id: modelId, input_field: inputField, output_field: vectorField, From c3019d6986230514b7fa4f4f66aab17943a17521 Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Thu, 18 Apr 2024 11:28:48 -0700 Subject: [PATCH 5/8] Generate ingest pipeline ID on the fly Signed-off-by: Tyler Ohlsen --- .../transformer/text_embedding_transformer.ts | 8 -------- .../utils/workflow_to_template_utils.ts | 12 +++++------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/public/component_types/transformer/text_embedding_transformer.ts b/public/component_types/transformer/text_embedding_transformer.ts index 88707813..5f7f4ff2 100644 --- a/public/component_types/transformer/text_embedding_transformer.ts +++ b/public/component_types/transformer/text_embedding_transformer.ts @@ -18,14 +18,6 @@ export class TextEmbeddingTransformer extends MLTransformer { this.baseClasses = [...this.baseClasses, this.type]; this.inputs = []; this.createFields = [ - { - label: 'Ingest Pipeline', - id: 'ingestPipelineName', - type: 'string', - helpText: - 'The name of the ingest pipeline configured with the text embedding transformer', - helpLink: 'https://opensearch.org/docs/latest/ingest-pipelines/', - }, { label: 'Model', id: 'model', diff --git a/public/pages/workflow_detail/utils/workflow_to_template_utils.ts b/public/pages/workflow_detail/utils/workflow_to_template_utils.ts index 713b126f..a514d0dd 100644 --- a/public/pages/workflow_detail/utils/workflow_to_template_utils.ts +++ b/public/pages/workflow_detail/utils/workflow_to_template_utils.ts @@ -28,6 +28,7 @@ import { MPNET_SENTENCE_TRANSFORMER, BERT_SENTENCE_TRANSFORMER, REGISTER_LOCAL_PRETRAINED_MODEL_STEP_TYPE, + generateId, } from '../../../../common'; /** @@ -128,18 +129,15 @@ function toIngestPipelineNodes(flowNode: ReactFlowComponent): TemplateNode[] { switch (flowNode.data.type) { case COMPONENT_CLASS.TEXT_EMBEDDING_TRANSFORMER: default: { - const { - ingestPipelineName, - model, - inputField, - vectorField, - } = componentDataToFormik(flowNode.data) as { - ingestPipelineName: string; + const { model, inputField, vectorField } = componentDataToFormik( + flowNode.data + ) as { model: ModelFormValue; inputField: string; vectorField: string; }; const modelId = model.id; + const ingestPipelineName = generateId('ingest_pipeline'); let registerModelStep = undefined as | RegisterPretrainedModelNode From 8994fbe0618ab6ac83a78b354d460370abf8758e Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Thu, 18 Apr 2024 13:04:37 -0700 Subject: [PATCH 6/8] remove readonly block once provisioned; add provision callout Signed-off-by: Tyler Ohlsen --- .../component_details/component_details.tsx | 7 +++++-- .../workspace/resizable_workspace.tsx | 15 ++++++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/public/pages/workflow_detail/component_details/component_details.tsx b/public/pages/workflow_detail/component_details/component_details.tsx index 41ed85d0..1de35468 100644 --- a/public/pages/workflow_detail/component_details/component_details.tsx +++ b/public/pages/workflow_detail/component_details/component_details.tsx @@ -28,9 +28,12 @@ interface ComponentDetailsProps { export function ComponentDetails(props: ComponentDetailsProps) { return ( - {props.isDeprovisionable ? ( + {/* TODO: determine if we need this view if we want the workspace to remain + readonly once provisioned */} + {/* {props.isDeprovisionable ? ( - ) : props.selectedComponent ? ( + ) : */} + {props.selectedComponent ? ( )} + {isDeprovisionable && isDirty && ( + + Changes cannot be saved until the flow has first been + deprovisioned. + + )} From ab0175644ddabf6e3ab80a158da3093277d1cfad Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Thu, 18 Apr 2024 13:30:16 -0700 Subject: [PATCH 7/8] Add algorithm field; clean up dropdown displays; Signed-off-by: Tyler Ohlsen --- common/constants.ts | 4 +++ common/interfaces.ts | 26 ++++++++++++++++++- .../transformer/text_embedding_transformer.ts | 2 +- .../input_fields/model_field.tsx | 14 +++++++++- public/utils/utils.ts | 1 + server/routes/helpers.ts | 4 ++- 6 files changed, 47 insertions(+), 4 deletions(-) diff --git a/common/constants.ts b/common/constants.ts index 87ef7960..6cdbbd63 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -4,6 +4,7 @@ */ import { + MODEL_ALGORITHM, PRETRAINED_MODEL_FORMAT, PretrainedSentenceTransformer, WORKFLOW_STATE, @@ -68,6 +69,7 @@ export const ROBERTA_SENTENCE_TRANSFORMER = { shortenedName: 'all-distilroberta-v1', description: 'A sentence transformer from Hugging Face', format: PRETRAINED_MODEL_FORMAT.TORCH_SCRIPT, + algorithm: MODEL_ALGORITHM.TEXT_EMBEDDING, version: '1.0.1', vectorDimensions: 768, } as PretrainedSentenceTransformer; @@ -77,6 +79,7 @@ export const MPNET_SENTENCE_TRANSFORMER = { shortenedName: 'all-mpnet-base-v2', description: 'A sentence transformer from Hugging Face', format: PRETRAINED_MODEL_FORMAT.TORCH_SCRIPT, + algorithm: MODEL_ALGORITHM.TEXT_EMBEDDING, version: '1.0.1', vectorDimensions: 768, } as PretrainedSentenceTransformer; @@ -86,6 +89,7 @@ export const BERT_SENTENCE_TRANSFORMER = { shortenedName: 'msmarco-distilbert-base-tas-b', description: 'A sentence transformer from Hugging Face', format: PRETRAINED_MODEL_FORMAT.TORCH_SCRIPT, + algorithm: MODEL_ALGORITHM.TEXT_EMBEDDING, version: '1.0.2', vectorDimensions: 768, } as PretrainedSentenceTransformer; diff --git a/common/interfaces.ts b/common/interfaces.ts index 99defd51..858965cf 100644 --- a/common/interfaces.ts +++ b/common/interfaces.ts @@ -152,6 +152,28 @@ export enum MODEL_STATE { DEPLOY_FAILED = 'Deploy failed', } +// Based off of https://github.com/opensearch-project/ml-commons/blob/main/common/src/main/java/org/opensearch/ml/common/FunctionName.java +export enum MODEL_ALGORITHM { + LINEAR_REGRESSION = 'Linear regression', + KMEANS = 'K-means', + AD_LIBSVM = 'AD LIBSVM', + SAMPLE_ALGO = 'Sample algorithm', + LOCAL_SAMPLE_CALCULATOR = 'Local sample calculator', + FIT_RCF = 'Fit RCF', + BATCH_RCF = 'Batch RCF', + ANOMALY_LOCALIZATION = 'Anomaly localization', + RCF_SUMMARIZE = 'RCF summarize', + LOGISTIC_REGRESSION = 'Logistic regression', + TEXT_EMBEDDING = 'Text embedding', + METRICS_CORRELATION = 'Metrics correlation', + REMOTE = 'Remote', + SPARSE_ENCODING = 'Sparse encoding', + SPARSE_TOKENIZE = 'Sparse tokenize', + TEXT_SIMILARITY = 'Text similarity', + QUESTION_ANSWERING = 'Question answering', + AGENT = 'Agent', +} + export enum MODEL_CATEGORY { DEPLOYED = 'Deployed', PRETRAINED = 'Pretrained', @@ -166,6 +188,7 @@ export type PretrainedModel = { shortenedName: string; description: string; format: PRETRAINED_MODEL_FORMAT; + algorithm: MODEL_ALGORITHM; version: string; }; @@ -181,7 +204,7 @@ export type ModelConfig = { export type Model = { id: string; name: string; - algorithm: string; + algorithm: MODEL_ALGORITHM; state: MODEL_STATE; modelConfig?: ModelConfig; }; @@ -193,6 +216,7 @@ export type ModelDict = { export type ModelFormValue = { id: string; category?: MODEL_CATEGORY; + algorithm?: MODEL_ALGORITHM; }; /** diff --git a/public/component_types/transformer/text_embedding_transformer.ts b/public/component_types/transformer/text_embedding_transformer.ts index 5f7f4ff2..affb996c 100644 --- a/public/component_types/transformer/text_embedding_transformer.ts +++ b/public/component_types/transformer/text_embedding_transformer.ts @@ -19,7 +19,7 @@ export class TextEmbeddingTransformer extends MLTransformer { this.inputs = []; this.createFields = [ { - label: 'Model', + label: 'Text Embedding Model', id: 'model', type: 'model', helpText: 'A text embedding model for embedding text.', diff --git a/public/pages/workflow_detail/component_details/input_fields/model_field.tsx b/public/pages/workflow_detail/component_details/input_fields/model_field.tsx index c8f4faa7..26ba8e37 100644 --- a/public/pages/workflow_detail/component_details/input_fields/model_field.tsx +++ b/public/pages/workflow_detail/component_details/input_fields/model_field.tsx @@ -84,6 +84,7 @@ export function ModelField(props: ModelFieldProps) { id: modelId, name: models[modelId].name, category: MODEL_CATEGORY.DEPLOYED, + algorithm: models[modelId].algorithm, } as ModelItem); } }); @@ -98,16 +99,19 @@ export function ModelField(props: ModelFieldProps) { id: ROBERTA_SENTENCE_TRANSFORMER.name, name: ROBERTA_SENTENCE_TRANSFORMER.shortenedName, category: MODEL_CATEGORY.PRETRAINED, + algorithm: ROBERTA_SENTENCE_TRANSFORMER.algorithm, }, { id: MPNET_SENTENCE_TRANSFORMER.name, name: MPNET_SENTENCE_TRANSFORMER.shortenedName, category: MODEL_CATEGORY.PRETRAINED, + algorithm: MPNET_SENTENCE_TRANSFORMER.algorithm, }, { id: BERT_SENTENCE_TRANSFORMER.name, name: BERT_SENTENCE_TRANSFORMER.shortenedName, category: MODEL_CATEGORY.PRETRAINED, + algorithm: BERT_SENTENCE_TRANSFORMER.algorithm, }, ]; setPretrainedModels(modelItems); @@ -170,10 +174,18 @@ export function ModelField(props: ModelFieldProps) { value: option.id, inputDisplay: ( <> - {option.name} + {option.name} + + ), + dropdownDisplay: ( + <> + {option.name} {option.category} + + {option.algorithm} + ), disabled: false, diff --git a/public/utils/utils.ts b/public/utils/utils.ts index 966202f9..76b6715e 100644 --- a/public/utils/utils.ts +++ b/public/utils/utils.ts @@ -102,6 +102,7 @@ export function getInitialValue(fieldType: FieldType): FieldValue { return { id: '', category: undefined, + algorithm: undefined, } as ModelFormValue; } case 'json': { diff --git a/server/routes/helpers.ts b/server/routes/helpers.ts index 44ed2f3b..afb404f7 100644 --- a/server/routes/helpers.ts +++ b/server/routes/helpers.ts @@ -6,6 +6,7 @@ import { DEFAULT_NEW_WORKFLOW_STATE_TYPE, INDEX_NOT_FOUND_EXCEPTION, + MODEL_ALGORITHM, MODEL_STATE, Model, ModelDict, @@ -94,7 +95,8 @@ export function getModelsFromResponses(modelHits: any[]): ModelDict { modelDict[modelId] = { id: modelId, name: modelHit._source?.name, - algorithm: modelHit._source?.algorithm, + // @ts-ignore + algorithm: MODEL_ALGORITHM[modelHit._source?.algorithm], // @ts-ignore state: MODEL_STATE[modelHit._source?.model_state], modelConfig: { From fc3fd574cea4ad567a3b90d38effc5fbcf6ac9a3 Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Thu, 18 Apr 2024 14:28:04 -0700 Subject: [PATCH 8/8] Refactoring and cleanup in workflow_to_template_utils Signed-off-by: Tyler Ohlsen --- .../utils/workflow_to_template_utils.ts | 119 +++++++----------- 1 file changed, 45 insertions(+), 74 deletions(-) diff --git a/public/pages/workflow_detail/utils/workflow_to_template_utils.ts b/public/pages/workflow_detail/utils/workflow_to_template_utils.ts index a514d0dd..399129d8 100644 --- a/public/pages/workflow_detail/utils/workflow_to_template_utils.ts +++ b/public/pages/workflow_detail/utils/workflow_to_template_utils.ts @@ -104,9 +104,9 @@ function toTemplateNodes( edges: ReactFlowEdge[] ): TemplateNode[] | undefined { if (flowNode.data.baseClasses?.includes(COMPONENT_CLASS.ML_TRANSFORMER)) { - return toIngestPipelineNodes(flowNode); + return transformerToTemplateNodes(flowNode); } else if (flowNode.data.baseClasses?.includes(COMPONENT_CLASS.INDEXER)) { - return [toIndexerNode(flowNode, prevNodes, edges)]; + return [indexerToTemplateNode(flowNode, prevNodes, edges)]; } } @@ -121,7 +121,9 @@ function toTemplateEdge(flowEdge: ReactFlowEdge): TemplateEdge { // ingest pipeline with a processor specific to the final class of the node. // Optionally prepend a register pretrained model step if the selected model // is a pretrained and undeployed one. -function toIngestPipelineNodes(flowNode: ReactFlowComponent): TemplateNode[] { +function transformerToTemplateNodes( + flowNode: ReactFlowComponent +): TemplateNode[] { // TODO a few improvements to make here: // 1. Consideration of multiple ingest processors and how to collect them all, and finally create // a single ingest pipeline with all of them, in the same order as done on the UI @@ -143,18 +145,14 @@ function toIngestPipelineNodes(flowNode: ReactFlowComponent): TemplateNode[] { | RegisterPretrainedModelNode | undefined; if (model.category === MODEL_CATEGORY.PRETRAINED) { - const pretrainedModelMap = {} as { - [modelName: string]: PretrainedSentenceTransformer; - }; - [ + const pretrainedModel = [ ROBERTA_SENTENCE_TRANSFORMER, MPNET_SENTENCE_TRANSFORMER, BERT_SENTENCE_TRANSFORMER, - ].map((transformer) => { - pretrainedModelMap[transformer.name] = transformer; - }); - // the model ID in the form will be the unique name of the pretrained model - const pretrainedModel = pretrainedModelMap[modelId]; + ].find( + // the model ID in the form will be the unique name of the pretrained model + (model) => model.name === modelId + ) as PretrainedSentenceTransformer; registerModelStep = { id: REGISTER_LOCAL_PRETRAINED_MODEL_STEP_TYPE, type: REGISTER_LOCAL_PRETRAINED_MODEL_STEP_TYPE, @@ -168,73 +166,46 @@ function toIngestPipelineNodes(flowNode: ReactFlowComponent): TemplateNode[] { } as RegisterPretrainedModelNode; } - // If we have a register model step, add it first, and use - // the produced model ID in the ingest pipeline step - if (registerModelStep !== undefined) { - return [ - registerModelStep, - { - id: flowNode.data.id, - type: CREATE_INGEST_PIPELINE_STEP_TYPE, - previous_node_inputs: { - [REGISTER_LOCAL_PRETRAINED_MODEL_STEP_TYPE]: 'model_id', - }, - user_inputs: { - pipeline_id: ingestPipelineName, - model_id: `\${{${REGISTER_LOCAL_PRETRAINED_MODEL_STEP_TYPE}.model_id}}`, - input_field: inputField, - output_field: vectorField, - configurations: { - description: - 'An ingest pipeline with a text embedding processor.', - processors: [ - { - text_embedding: { - model_id: `\${{${REGISTER_LOCAL_PRETRAINED_MODEL_STEP_TYPE}.model_id}}`, - field_map: { - [inputField]: vectorField, - }, - }, - } as TextEmbeddingProcessor, - ], - }, - }, - } as CreateIngestPipelineNode, - ]; - } else { - return [ - { - id: flowNode.data.id, - type: CREATE_INGEST_PIPELINE_STEP_TYPE, - user_inputs: { - pipeline_id: ingestPipelineName, - model_id: modelId, - input_field: inputField, - output_field: vectorField, - configurations: { - description: - 'An ingest pipeline with a text embedding processor.', - processors: [ - { - text_embedding: { - model_id: modelId, - field_map: { - [inputField]: vectorField, - }, - }, - } as TextEmbeddingProcessor, - ], - }, - }, - } as CreateIngestPipelineNode, - ]; - } + // The model ID depends on if we are consuming it from a previous pretrained model step, + // or directly from the user + const finalModelId = + registerModelStep !== undefined + ? `\${{${REGISTER_LOCAL_PRETRAINED_MODEL_STEP_TYPE}.model_id}}` + : modelId; + + const createIngestPipelineStep = { + id: flowNode.data.id, + type: CREATE_INGEST_PIPELINE_STEP_TYPE, + user_inputs: { + pipeline_id: ingestPipelineName, + model_id: finalModelId, + input_field: inputField, + output_field: vectorField, + configurations: { + description: 'An ingest pipeline with a text embedding processor.', + processors: [ + { + text_embedding: { + model_id: finalModelId, + field_map: { + [inputField]: vectorField, + }, + }, + } as TextEmbeddingProcessor, + ], + }, + }, + } as CreateIngestPipelineNode; + + return registerModelStep !== undefined + ? [registerModelStep, createIngestPipelineStep] + : [createIngestPipelineStep]; } } } // General fn to convert an indexer node to a final CreateIndexNode template node. -function toIndexerNode( +function indexerToTemplateNode( flowNode: ReactFlowComponent, prevNodes: ReactFlowComponent[], edges: ReactFlowEdge[]